diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 51fe7a7bd1..44e66fe9cd 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -36,6 +36,7 @@ body: - [ ] Push `main` and the new tag `v1.1.10` to origin - [ ] Checkout `develop` - [ ] Increase version in `./vector/build.gradle` + - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - [ ] Commit and push `develop` - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. @@ -72,9 +73,25 @@ body: - [ ] Create a release with GitFlow - [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. - [ ] Run the script `./tools/import_from_element.sh` - - [ ] Update the version in `./matrix-sdk-android/build.gradle` and let the script finish to build the library + - [ ] Update the 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"`) + - [ ] 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 + + ##### Release on MavenCentral + + - [ ] Run the command `./gradlew publish --no-daemon --no-parallel`. You'll need some non-public element to do so + - [ ] Connect to https://s01.oss.sonatype.org + - [ ] Click on Staging Repositories and check the the files have been uploaded + - [ ] Click on close + - [ ] Wait (check Activity tab until step "Repository closed" is displayed) + - [ ] Click on release. The staging repository will disappear + - [ ] Check that the release is available in https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ (it can take a few minutes) + + ##### Release on GitHub + - [ ] Create the release on GitHub from [the tag](https://github.com/matrix-org/matrix-android-sdk2/tags) - [ ] Upload the AAR on the GitHub release @@ -82,7 +99,7 @@ body: https://github.com/matrix-org/matrix-android-sdk2-sample - - [ ] Update the dependency to the new version of the SDK2. Jitpack will have to build the AAR, it can take a few minutes. You can check status on https://jitpack.io/#matrix-org/matrix-android-sdk2 + - [ ] Update the dependency to the new version of the SDK2. It can take some time for MavenCentral to make the librarie available. You can check status on https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ - [ ] Build and run the sample, you may have to fix some API break - [ ] Commit and push directly on `main` validations: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85148a2632..91dc6d830b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,12 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: debug: name: Build debug APKs (${{ matrix.target }}) @@ -25,7 +31,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Assemble ${{ matrix.target }} debug apk - run: ./gradlew assemble${{ matrix.target }}Debug --stacktrace + run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES --stacktrace - name: Upload ${{ matrix.target }} debug APKs uses: actions/upload-artifact@v2 with: @@ -48,7 +54,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Assemble GPlay unsigned apk - run: ./gradlew clean assembleGplayRelease --stacktrace + run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES --stacktrace - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 984ae0748e..c18ca69fde 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -5,6 +5,12 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: # Temporary add build of Android tests, which cannot be run on the CI right now, but they need to at least compile # So it will be mandatory for this action to be successful on every PRs @@ -22,7 +28,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Compile Android tests - run: ./gradlew clean assembleAndroidTest --stacktrace -PallWarningsAsErrors=false + run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace -PallWarningsAsErrors=false integration-tests: name: Integration Tests (Synapse) @@ -30,9 +36,14 @@ jobs: strategy: fail-fast: false matrix: - api-level: [21, 28, 30] + api-level: [28] steps: - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: 11 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: @@ -64,5 +75,12 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + #arch: x86_64 + #disable-animations: true # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest - script: ./gradlew -PallWarningsAsErrors=false connectedCheck + arch: x86 + profile: Nexus 5X + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-build: 7425822 + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 632dee8a58..3ab0017ce2 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -5,6 +5,12 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: integration-tests: name: Sanity Tests (Synapse) @@ -46,5 +52,5 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - script: ./gradlew -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e51368ce5..50195638de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,12 @@ on: push: branches: [main, develop] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: unit-tests: name: Run Unit Tests @@ -20,4 +26,11 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Run unit tests - run: ./gradlew clean test --stacktrace -PallWarningsAsErrors=false + run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() && + github.event.sender.login != 'dependabot[bot]' && + ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) + with: + files: ./**/build/test-results/**/*.xml diff --git a/.gitignore b/.gitignore index 04d1b6fe06..935a3fc329 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .idea/*.xml .DS_Store /build +/benchmark-out /captures .externalNativeBuild diff --git a/CHANGES.md b/CHANGES.md index 472b422a1c..a0b3e7e2ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,53 @@ +Changes in Element v1.3.0 (2021-09-27) +====================================== + +Features ✨ +---------- + - Spaces! + - Adds email notification registration to Settings ([#2243](https://github.com/vector-im/element-android/issues/2243)) + - Spaces | M3.23 Invite by email in create private space flow ([#3678](https://github.com/vector-im/element-android/issues/3678)) + - Improve space invite bottom sheet ([#4057](https://github.com/vector-im/element-android/issues/4057)) + - Allow to also leave rooms when leaving a space ([#3692](https://github.com/vector-im/element-android/issues/3692)) + - Better expose adding spaces as Subspaces ([#3752](https://github.com/vector-im/element-android/issues/3752)) + - Push and syncs: add debug info on room list and on room detail screen and improves the log format. ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Bugfixes 🐛 +---------- + - Remove the "Teammate spaces aren't quite ready" bottom sheet ([#3945](https://github.com/vector-im/element-android/issues/3945)) + - Restricted Room previews aren't working ([#3946](https://github.com/vector-im/element-android/issues/3946)) + - A removed room from a space can't be re-added as it won't be shown in add-room ([#3947](https://github.com/vector-im/element-android/issues/3947)) + - "Non-Admin" user able to invite others to Private Space (by default) ([#3951](https://github.com/vector-im/element-android/issues/3951)) + - Kick user dialog for spaces talks about rooms ([#3956](https://github.com/vector-im/element-android/issues/3956)) + - Messages are displayed as unable to decrypt then decrypted a few seconds later ([#4011](https://github.com/vector-im/element-android/issues/4011)) + - Fix DTMF not working ([#4015](https://github.com/vector-im/element-android/issues/4015)) + - Fix sticky end call notification ([#4019](https://github.com/vector-im/element-android/issues/4019)) + - Fix call screen stuck with some hanging up scenarios ([#4026](https://github.com/vector-im/element-android/issues/4026)) + - Fix other call not always refreshed when ended ([#4028](https://github.com/vector-im/element-android/issues/4028)) + - Private space invite bottomsheet only offering inviting by username not by email ([#4042](https://github.com/vector-im/element-android/issues/4042)) + - Spaces invitation system notifications don't take me to the join space toast ([#4043](https://github.com/vector-im/element-android/issues/4043)) + - Space Invites are not lighting up the drawer menu ([#4059](https://github.com/vector-im/element-android/issues/4059)) + - MessageActionsBottomSheet not being shown on local echos ([#4068](https://github.com/vector-im/element-android/issues/4068)) + +SDK API changes ⚠️ +------------------ + - InitialSyncProgressService has been renamed to SyncStatusService and its function getInitialSyncProgressStatus() has been renamed to getSyncStatusLive() ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Other changes +------------- + - Better support for Sdk2 version. Also slight change in the default user agent: `MatrixAndroidSDK_X` is replaced by `MatrixAndroidSdk2` ([#3994](https://github.com/vector-im/element-android/issues/3994)) + - Introduces ConferenceEvent to abstract usage of Jitsi BroadcastEvent class. ([#4014](https://github.com/vector-im/element-android/issues/4014)) + - Improve performances on RoomDetail screen ([#4065](https://github.com/vector-im/element-android/issues/4065)) + + +Changes in Element v1.2.2 (2021-09-13) +====================================== + +Bugfixes 🐛 +---------- + +- Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing for details. + + Changes in Element v1.2.1 (2021-09-08) ====================================== diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index d62062ae14..064f497dc7 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -18,13 +18,12 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk } buildTypes { @@ -34,8 +33,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { jvmTarget = "11" @@ -51,13 +50,13 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation libs.rx.rxKotlin + implementation libs.rx.rxAndroid - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation libs.jetbrains.kotlinStdlib + implementation libs.androidx.core + implementation libs.androidx.appCompat + implementation libs.androidx.recyclerview - implementation 'com.google.android.material:material:1.4.0' + implementation libs.google.material } \ No newline at end of file diff --git a/build.gradle b/build.gradle index ef26530298..49c3e07ece 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.5.21' - ext.kotlin_coroutines_version = "1.5.0" + + apply from: 'dependencies.gradle' + repositories { google() jcenter() @@ -11,12 +11,13 @@ buildscript { url "https://plugins.gradle.org/m2/" } } + dependencies { // Release notes of Android Gradle Plugin (AGP): // https://developer.android.com/studio/releases/gradle-plugin - classpath 'com.android.tools.build:gradle:7.0.2' + classpath libs.gradle.gradlePlugin + classpath libs.gradle.kotlinPlugin classpath 'com.google.gms:google-services:4.3.10' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' classpath "com.likethesalad.android:string-reference:1.2.2" diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000000..a4e2c60387 --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,130 @@ +ext.versions = [ + + 'minSdk' : 21, + 'compileSdk' : 30, + 'targetSdk' : 30, + 'sourceCompat' : JavaVersion.VERSION_11, + 'targetCompat' : JavaVersion.VERSION_11, +] + +def gradle = "7.0.2" +// Ref: https://kotlinlang.org/releases.html +def kotlin = "1.5.30" +def kotlinCoroutines = "1.5.1" +def dagger = "2.38.1" +def retrofit = "2.9.0" +def arrow = "0.8.2" +def markwon = "4.6.2" +def moshi = "1.12.0" +def lifecycle = "2.2.0" +def rxBinding = "3.1.0" +def epoxy = "4.6.2" +def glide = "4.12.0" +def bigImageViewer = "1.8.1" +def jjwt = "0.11.2" + +// Testing +def mockk = "1.12.0" +def espresso = "3.4.0" +def androidxTest = "1.4.0" + + +ext.libs = [ + gradle : [ + 'gradlePlugin' : "com.android.tools.build:gradle:$gradle", + 'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin" + ], + jetbrains : [ + 'kotlinStdlibJdk7' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin", + 'kotlinStdlib' : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin", + 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", + 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", + 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines" + ], + androidx : [ + 'appCompat' : "androidx.appcompat:appcompat:1.3.1", + 'core' : "androidx.core:core-ktx:1.6.0", + 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", + 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.0", + 'work' : "androidx.work:work-runtime-ktx:2.5.0", + 'autoFill' : "androidx.autofill:autofill:1.1.0", + 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", + 'junit' : "androidx.test.ext:junit:1.1.3", + 'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle", + 'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle", + 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1", + 'datastore' : "androidx.datastore:datastore:1.0.0", + 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", + 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", + 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", + 'testCore' : "androidx.test:core:$androidxTest", + 'orchestrator' : "androidx.test:orchestrator:$androidxTest", + 'testRunner' : "androidx.test:runner:$androidxTest", + 'testRules' : "androidx.test:rules:$androidxTest", + 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", + 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", + 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" + ], + google : [ + 'material' : "com.google.android.material:material:1.4.0" + ], + dagger : [ + 'dagger' : "com.google.dagger:dagger:$dagger", + 'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger" + ], + squareup : [ + 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi", + 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", + 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", + 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" + ], + rx : [ + 'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0", + 'rxAndroid' : "io.reactivex.rxjava2:rxandroid:2.1.1" + ], + arrow : [ + 'core' : "io.arrow-kt:arrow-core:$arrow", + 'instances' : "io.arrow-kt:arrow-instances-core:$arrow" + ], + markwon : [ + 'core' : "io.noties.markwon:core:$markwon", + 'html' : "io.noties.markwon:html:$markwon" + ], + airbnb : [ + 'epoxy' : "com.airbnb.android:epoxy:$epoxy", + 'epoxyGlide' : "com.airbnb.android:epoxy-glide-preloading:$epoxy", + 'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy", + 'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy", + 'mvrx' : "com.airbnb.android:mvrx:1.5.1" + ], + mockk : [ + 'mockk' : "io.mockk:mockk:$mockk", + 'mockkAndroid' : "io.mockk:mockk-android:$mockk" + ], + github : [ + 'glide' : "com.github.bumptech.glide:glide:$glide", + 'glideCompiler' : "com.github.bumptech.glide:compiler:$glide", + 'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer", + 'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer", + 'progressPieIndicator' : "com.github.piasy:ProgressPieIndicator:$bigImageViewer", + 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer" + ], + jakewharton : [ + 'timber' : "com.jakewharton.timber:timber:5.0.1", + 'rxbinding' : "com.jakewharton.rxbinding3:rxbinding:$rxBinding", + 'rxbindingAppcompat' : "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBinding", + 'rxbindingMaterial' : "com.jakewharton.rxbinding3:rxbinding-material:$rxBinding" + ], + jsonwebtoken: [ + 'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt", + 'jjwtImpl' : "io.jsonwebtoken:jjwt-impl:$jjwt", + 'jjwtOrgjson' : "io.jsonwebtoken:jjwt-orgjson:$jjwt" + ], + tests : [ + 'kluent' : "org.amshove.kluent:kluent-android:1.68", + 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", + 'junit' : "junit:junit:4.13.2" + ] +] \ No newline at end of file diff --git a/fastlane/metadata/android/de-DE/changelogs/40102000.txt b/fastlane/metadata/android/de-DE/changelogs/40102000.txt new file mode 100644 index 0000000000..9375289279 --- /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.1.16 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/fr-FR/changelogs/40102000.txt b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt new file mode 100644 index 0000000000..504c3e24be --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : messages vocaux activés par défault. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/changelogs/40101160.txt b/fastlane/metadata/android/id/changelogs/40101160.txt new file mode 100644 index 0000000000..19209bacf2 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Memperbaiki kesalahan saat mengirim pesan terenkripsi jika seseorang yang ada di ruangan keluar. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/changelogs/40102000.txt b/fastlane/metadata/android/id/changelogs/40102000.txt new file mode 100644 index 0000000000..2258b114e8 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Pesan Suara diaktifkan secara default +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt index 0a18b8d64a..75249c6a20 100644 --- a/fastlane/metadata/android/id/full_description.txt +++ b/fastlane/metadata/android/id/full_description.txt @@ -8,10 +8,10 @@ Element adalah perpesanan yang aman dan aplikasi kolaborasi tim produktivitas ya - Obrolan video dengan VoIP dan berbagi layar - Integrasi yang mudah dengan alat kolaborasi online favorit Anda, alat manajemen proyek, layanan VoIP dan aplikasi perpesanan tim lainnya -Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Ini beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Ini memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. +Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Element beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Matrix memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. Pesan privasi dan terenkripsi -Element melindungi Anda dari iklan yang tidak diinginkan, data penambangan dan taman berdinding. Ini juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang di-cross-signed. +Element melindungi Anda dari iklan yang tidak diinginkan, data penambangan dan taman berdinding. Element juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang ditanda tangani silang. Element memberi Anda kendali atas privasi Anda sambil memungkinkan Anda untuk berkomunikasi dengan aman dengan siapa pun di jaringan Matrix, atau alat kolaborasi bisnis lainnya dengan mengintegrasikan dengan aplikasi seperti Slack. @@ -30,7 +30,7 @@ Element menempatkan Anda dalam kendali dengan cara yang berbeda: Anda dapat mengobrol dengan siapa saja di jaringan Matrix, apakah mereka menggunakan Element, aplikasi Matrix lain atau bahkan jika mereka menggunakan aplikasi perpesanan yang berbeda. Sangat aman -Enkripsi ujung-ke-ujung beneran (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat yang di-cross-signed. +Enkripsi ujung-ke-ujung beneran (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat yang ditanda tangani silang. Komunikasi dan integrasi lengkap Perpesanan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal. diff --git a/fastlane/metadata/android/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..ab0dd0237d --- /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.1.16 diff --git a/gradle.properties b/gradle.properties index 200866be25..98d561815b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,20 +6,20 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx2048m -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# Enable file system watch (https://docs.gradle.org/6.7/release-notes.html) +# Build Time Optimizations +org.gradle.jvmargs=-Xmx3g -Xms512M -XX:MaxPermSize=2048m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.configureondemand=true +org.gradle.parallel=true org.gradle.vfs.watch=true +# Android Settings +android.enableJetifier=true +android.useAndroidX=true + +#Project Settings +# Change debugPrivateData to true for debugging vector.debugPrivateData=false +# httpLogLevel values: NONE, BASIC, HEADERS, BODY vector.httpLogLevel=BASIC -# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above -#vector.debugPrivateData=true -#vector.httpLogLevel=BODY diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 365e4b902c..cee58414c7 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -20,14 +20,11 @@ plugins { } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -41,8 +38,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { @@ -55,10 +52,10 @@ android { } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation libs.androidx.appCompat + implementation libs.google.material // Pref theme - implementation 'androidx.preference:preference-ktx:1.1.1' + implementation libs.androidx.preferenceKtx // PFLockScreen attrs implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen diff --git a/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml b/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml index d6fa160d6b..b0c45b1fea 100644 --- a/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml +++ b/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml @@ -8,6 +8,7 @@ + diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index d18e3b1d72..dbd761cee3 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -3,13 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk // Multidex is useful for tests multiDexEnabled true @@ -24,8 +22,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { @@ -34,15 +32,16 @@ android { } dependencies { + implementation project(":matrix-sdk-android") - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" + implementation libs.androidx.appCompat + implementation libs.rx.rxKotlin + implementation libs.rx.rxAndroid + implementation libs.jetbrains.coroutinesRx2 // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation libs.androidx.pagingRuntimeKtx // Logging - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation libs.jakewharton.timber } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 58fb760ff5..47203816b4 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -239,6 +240,10 @@ class RxSession(private val session: Session) { ) .distinctUntilChanged() } + + fun lookupThreePid(threePid: ThreePid): Single> = rxSingle { + session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional() + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index cbae6a05b3..f50a6c2e92 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -14,14 +14,14 @@ buildscript { } android { - compileSdkVersion 30 testOptions.unitTests.includeAndroidResources = true + compileSdk versions.compileSdk + defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "0.0.1" + minSdk versions.minSdk + targetSdk versions.targetSdk + // Multidex is useful for tests multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -31,9 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - // Seems that the build tools 4.1.0 does not generate BuildConfig.VERSION_NAME anymore. - // Add it manually here. We may remove this trick in the future - buildConfigField "String", "VERSION_NAME", "\"0.0.1\"" + buildConfigField "String", "SDK_VERSION", "\"1.2.2\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" @@ -68,8 +66,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { @@ -103,92 +101,83 @@ static def gitRevisionDate() { dependencies { - def arrow_version = "0.8.2" - def moshi_version = '1.12.0' - def lifecycle_version = '2.2.0' - def arch_version = '2.1.0' - def markwon_version = '3.1.0' - def daggerVersion = '2.38.1' - def work_version = '2.5.0' - def retrofit_version = '2.9.0' + implementation libs.jetbrains.kotlinStdlibJdk7 + implementation libs.jetbrains.coroutinesCore + implementation libs.jetbrains.coroutinesAndroid - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + implementation libs.androidx.appCompat + implementation libs.androidx.core - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.core:core-ktx:1.6.0" - - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + implementation libs.androidx.lifecycleExtensions + implementation libs.androidx.lifecycleJava8 // Network - implementation "com.squareup.retrofit2:retrofit:$retrofit_version" - implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation libs.squareup.retrofit + implementation libs.squareup.retrofitMoshi implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.1")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.okhttp3:okhttp-urlconnection' - implementation "com.squareup.moshi:moshi-adapters:$moshi_version" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + implementation libs.squareup.moshi + kapt libs.squareup.moshiKotlin - implementation "ru.noties.markwon:core:$markwon_version" + implementation libs.markwon.core // Image - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation libs.androidx.exifinterface // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' kapt 'dk.ilios:realmfieldnameshelper:2.0.0' // Work - implementation "androidx.work:work-runtime-ktx:$work_version" + implementation libs.androidx.work // FP - implementation "io.arrow-kt:arrow-core:$arrow_version" - implementation "io.arrow-kt:arrow-instances-core:$arrow_version" + implementation libs.arrow.core + implementation libs.arrow.instances // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm implementation 'org.matrix.gitlab.matrix-org:olm:3.2.4' // DI - implementation "com.google.dagger:dagger:$daggerVersion" - kapt "com.google.dagger:dagger-compiler:$daggerVersion" + implementation libs.dagger.dagger + kapt libs.dagger.daggerCompiler // Logging - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation libs.jakewharton.timber implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression - implementation 'com.otaliastudios:transcoder:0.10.3' + implementation 'com.otaliastudios:transcoder:0.10.4' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.33' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.5.1' + testImplementation libs.tests.junit + testImplementation 'org.robolectric:robolectric:4.6.1' //testImplementation 'org.robolectric:shadows-support-v4:3.0' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 - testImplementation 'io.mockk:mockk:1.12.0' - testImplementation 'org.amshove.kluent:kluent-android:1.68' - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + testImplementation libs.mockk.mockk + testImplementation libs.tests.kluent + implementation libs.jetbrains.coroutinesAndroid // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' - kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" - androidTestImplementation 'androidx.test:core:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'org.amshove.kluent:kluent-android:1.68' - androidTestImplementation 'io.mockk:mockk-android:1.12.0' - androidTestImplementation "androidx.arch.core:core-testing:$arch_version" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + kaptAndroidTest libs.dagger.daggerCompiler + androidTestImplementation libs.androidx.testCore + androidTestImplementation libs.androidx.testRunner + androidTestImplementation libs.androidx.testRules + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espressoCore + androidTestImplementation libs.tests.kluent + androidTestImplementation libs.mockk.mockkAndroid + androidTestImplementation libs.androidx.coreTesting + androidTestImplementation libs.jetbrains.coroutinesAndroid // Plant Timber tree for test - androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + androidTestImplementation libs.tests.timberJunitRule - androidTestUtil 'androidx.test:orchestrator:1.4.0' + androidTestUtil libs.androidx.orchestrator } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index c439da8407..8b9b6efa11 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -117,7 +117,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo } fun getSdkVersion(): String { - return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + return BuildConfig.SDK_VERSION + " (" + BuildConfig.GIT_SDK_REVISION + ")" } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 7817351e53..cf9b8f87c1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -19,6 +19,8 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri import androidx.lifecycle.Observer +import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -59,13 +61,15 @@ class CommonTestHelper(context: Context) { fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor init { - Matrix.initialize( - context, - MatrixConfiguration( - applicationFlavor = "TestFlavor", - roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() - ) - ) + UiThreadStatement.runOnUiThread { + Matrix.initialize( + context, + MatrixConfiguration( + applicationFlavor = "TestFlavor", + roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() + ) + ) + } matrix = Matrix.getInstance(context) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 301cdea461..436daf001b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -32,10 +32,19 @@ import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.query.ActiveSpaceFilter +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.SessionTestParams @@ -386,6 +395,8 @@ class SpaceHierarchyTest : InstrumentedTest { // The room should have disapear from flat children GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } } + + commonTestHelper.signOutAndClose(session) } data class TestSpaceCreationResult( @@ -434,6 +445,57 @@ class SpaceHierarchyTest : InstrumentedTest { return TestSpaceCreationResult(spaceId, roomIds) } + @Suppress("EXPERIMENTAL_API_USAGE") + private fun createPrivateSpace(session: Session, + spaceName: String, + childInfo: List> + /** Name, auto-join, canonical*/ + ): TestSpaceCreationResult { + var spaceId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + it.countDown() + } + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = + childInfo.map { entry -> + var roomId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val homeServerCapabilities = session + .getHomeServerCapabilities() + roomId = session.createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) + it.countDown() + } + } + roomId + } + + roomIds.forEachIndexed { index, roomId -> + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + } + } + } + return TestSpaceCreationResult(spaceId, roomIds) + } + @Test fun testRootSpaces() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -473,5 +535,111 @@ class SpaceHierarchyTest : InstrumentedTest { val rootSpaces = session.spaceService().getRootSpaceSummaries() assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun testParentRelation() { + val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) + + val spaceAInfo = createPrivateSpace(aliceSession, "Private Space A", listOf( + Triple("General", true /*suggested*/, true/*canonical*/), + Triple("Random", true, true) + )) + + commonTestHelper.runBlockingTest { + aliceSession.getRoom(spaceAInfo.spaceId)!!.invite(bobSession.myUserId, null) + } + + commonTestHelper.runBlockingTest { + bobSession.joinRoom(spaceAInfo.spaceId, null, emptyList()) + } + + var bobRoomId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + bobRoomId = bobSession.createRoom(CreateRoomParams().apply { name = "A Bob Room" }) + bobSession.getRoom(bobRoomId)!!.invite(aliceSession.myUserId) + it.countDown() + } + } + + commonTestHelper.runBlockingTest { + aliceSession.joinRoom(bobRoomId) + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) + stateEvent != null + } + } + + // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space + commonTestHelper.runBlockingTest { + // we can see the state event + // but it is not valid and room is not in hierarchy + assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) + } + + // Let's now try to make alice admin of the room + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val room = bobSession.getRoom(bobRoomId)!! + val currentPLContent = room + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.let { it.content.toModel() } + + val newPowerLevelsContent = currentPLContent + ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) + ?.toContent() + + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent!!) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } + powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true + } + } + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 9980259266..8a4526a5e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -111,7 +111,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo } fun getSdkVersion(): String { - return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + return BuildConfig.SDK_VERSION + " (" + BuildConfig.GIT_SDK_REVISION + ")" } } } diff --git a/matrix-sdk-android/src/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/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 2f981ffbbe..1443a8d3b9 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 @@ -36,7 +36,7 @@ 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 @@ -75,7 +75,7 @@ interface Session : ProfileService, PushRuleService, PushersService, - InitialSyncProgressService, + SyncStatusService, HomeServerCapabilitiesService, SecureStorageService, AccountService { 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 3d82846e7e..1f8471c111 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 @@ -239,7 +239,7 @@ data class Event( fun Event.isTextMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE -> true @@ -249,7 +249,7 @@ fun Event.isTextMessage(): Boolean { fun Event.isImageMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_IMAGE -> true else -> false } @@ -257,7 +257,7 @@ fun Event.isImageMessage(): Boolean { fun Event.isVideoMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_VIDEO -> true else -> false } @@ -265,7 +265,7 @@ fun Event.isVideoMessage(): Boolean { fun Event.isAudioMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_AUDIO -> true else -> false } @@ -273,7 +273,7 @@ fun Event.isAudioMessage(): Boolean { fun Event.isFileMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_FILE -> true else -> false } @@ -281,7 +281,7 @@ fun Event.isFileMessage(): Boolean { fun Event.isAttachmentMessage(): Boolean { return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { + && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_VIDEO, 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..38d47ae1a9 --- /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/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..2cd17952c6 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,14 +27,12 @@ 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 @@ -64,6 +62,30 @@ interface PushersService { append: Boolean, withEventIdOnly: Boolean): 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. + * @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 addEmailPusher(email: String, + lang: String, + emailBranding: String, + appDisplayName: String, + deviceDisplayName: String, + append: Boolean = true): UUID + /** * Directly ask the push gateway to send a push to this device * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. @@ -80,10 +102,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 */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomStrippedState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomStrippedState.kt new file mode 100644 index 0000000000..dc0c00b282 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomStrippedState.kt @@ -0,0 +1,111 @@ +/* + * 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.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * These are the same fields as those returned by /publicRooms, with a few additions: room_type, membership and is_encrypted. + */ +@JsonClass(generateAdapter = true) +data class RoomStrippedState( + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false, + + /** + * Optional. If the room is encrypted. This is already accessible as stripped state. + */ + @Json(name = "is_encrypted") + val isEncrypted: Boolean?, + + /** + * Optional. Type of the room, if any, i.e. m.space + */ + @Json(name = "room_type") + val roomType: String?, + + /** + * The current membership of this user in the room. Usually leave if the room is fetched over federation. + */ + @Json(name = "membership") + val membership: String? +) { + /** + * Return the canonical alias, or the first alias from the list of aliases, or null + */ + fun getPrimaryAlias(): String? { + return canonicalAlias ?: aliases?.firstOrNull() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt index 1bcb10d88c..ebf3d127ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -28,7 +28,7 @@ data class MessageAudioContent( /** * Required. Must be 'm.audio'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt index df5641a622..5a1b66c91c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt @@ -20,6 +20,11 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent interface MessageContent { + + companion object { + const val MSG_TYPE_JSON_KEY = "msgtype" + } + val msgType: String val body: String val relatesTo: RelationDefaultContent? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt index 65e89cdfee..1dadc92271 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon @JsonClass(generateAdapter = true) data class MessageDefaultContent( - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, @Json(name = "body") override val body: String, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt index 77983a031e..a2ada416ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt @@ -26,7 +26,7 @@ data class MessageEmoteContent( /** * Required. Must be 'm.emote'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. The emote action to perform. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt index 96877b4d9f..78f9a5d2f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt @@ -28,7 +28,7 @@ data class MessageFileContent( /** * Required. Must be 'm.file'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. A human-readable description of the file. This is recommended to be the filename of the original upload. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt index 73fd1eab56..ea7ab50688 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt @@ -27,7 +27,7 @@ data class MessageImageContent( /** * Required. Must be 'm.image'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index bdb54910dd..6881c09924 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -26,7 +26,7 @@ data class MessageLocationContent( /** * Required. Must be 'm.location'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt index b2fd8cb0c0..dd960355ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt @@ -26,7 +26,7 @@ data class MessageNoticeContent( /** * Required. Must be 'm.notice'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. The notice text to send. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt index 7924469884..7a1a99bd5f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt @@ -30,7 +30,7 @@ const val OPTION_TYPE_BUTTONS = "org.matrix.buttons" */ @JsonClass(generateAdapter = true) data class MessageOptionsContent( - @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_OPTIONS, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_OPTIONS, @Json(name = "type") val optionType: String? = null, @Json(name = "body") override val body: String, @Json(name = "label") val label: String?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt index d827475277..9edfe118b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon */ @JsonClass(generateAdapter = true) data class MessagePollResponseContent( - @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_RESPONSE, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_RESPONSE, @Json(name = "body") override val body: String, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt index e45245a9bf..5968fecd43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt @@ -26,7 +26,7 @@ data class MessageTextContent( /** * Required. Must be 'm.text'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, /** * Required. The body of the message. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt index 25b5f44815..b2b3cdac90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReque @JsonClass(generateAdapter = true) data class MessageVerificationRequestContent( - @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY)override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, @Json(name = "body") override val body: String, @Json(name = "from_device") override val fromDevice: String?, @Json(name = "methods") override val methods: List, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt index 3f5d2dab2e..e1b0cd8607 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt @@ -27,7 +27,7 @@ data class MessageVideoContent( /** * Required. Must be 'm.video'. */ - @Json(name = "msgtype") override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY)override val msgType: String, /** * Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'. 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 bcc36b579a..f40572518f 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 @@ -94,5 +94,7 @@ interface SpaceService { */ suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List) + suspend fun removeSpaceParent(childRoomId: String, parentSpaceId: String) + fun getRootSpaceSummaries(): List } 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..3825a5dab2 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 @@ -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/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/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt index 1a88404128..57eab6a8dd 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 @@ -36,7 +36,7 @@ internal class UserAgentHolder @Inject constructor(private val context: Context, /** * Create an user agent with the application version. - * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSDK_X 1.0) + * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) * * @param flavorDescription the flavor description */ @@ -74,13 +74,13 @@ 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 - + "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")") + + "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") } else { // update userAgent = appName + "/" + appVersion + " " + systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + "; Flavour " + flavorDescription + - "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")" + "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")" } } } 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..22167bc77a 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 @@ -40,7 +40,7 @@ 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.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 @@ -115,7 +115,7 @@ 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, @@ -141,7 +141,7 @@ 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(), 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..2003a66c94 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 @@ -43,7 +43,7 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan 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.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 @@ -127,7 +127,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/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index cb29cb4819..dc59277f64 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 @@ -355,7 +355,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/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index fdb6caf53f..acd163450c 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,16 @@ 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.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 +42,17 @@ 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.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers 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 @@ -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 } 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..6dac9bffd0 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/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt index c9d7ad2193..079fd1d3e5 100644 --- 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/AddPusherWorker.kt @@ -33,8 +33,8 @@ 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) { +internal class AddPusherWorker(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/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index a772cf5ebb..9a50abfe35 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 @@ -66,27 +66,45 @@ internal class DefaultPushersService @Inject constructor( 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'") + withEventIdOnly: Boolean + ) = addPusher( + JsonPusher( + pushKey = pushkey, + kind = Pusher.KIND_HTTP, + appId = appId, + profileTag = profileTag, + lang = lang, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), + append = append + ) + ) - 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 fun addEmailPusher(email: String, + lang: String, + emailBranding: String, + appDisplayName: String, + deviceDisplayName: String, + append: Boolean + ) = addPusher( + 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 + ) + ) - val params = AddHttpPusherWorker.Params(sessionId, pusher) - - val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() + private fun addPusher(pusher: JsonPusher): UUID { + pusher.validateParameters() + 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 +113,27 @@ internal class DefaultPushersService @Inject constructor( return request.id } + private fun JsonPusher.validateParameters() { + // 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'") } + } + + 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/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/room/GetRoomSummaryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/GetRoomSummaryTask.kt new file mode 100644 index 0000000000..d9547d9e3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/GetRoomSummaryTask.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.room + +import org.matrix.android.sdk.api.session.room.model.RoomStrippedState +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 javax.inject.Inject + +internal interface GetRoomSummaryTask : Task { + data class Params( + val roomId: String, + val viaServers: List? + ) +} + +internal class DefaultGetRoomSummaryTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetRoomSummaryTask { + + override suspend fun execute(params: GetRoomSummaryTask.Params): RoomStrippedState { + return executeRequest(globalErrorReceiver) { + roomAPI.getRoomSummary(params.roomId, params.viaServers) + } + } +} 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 535fa9df71..98e7659238 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse import org.matrix.android.sdk.api.util.JsonDict @@ -254,7 +255,7 @@ internal interface RoomAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") suspend fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, @Query("server_name") viaServers: List, - @Body params: JsonDict): JoinRoomResponse + @Body params: JsonDict): JoinRoomResponse /** * Leave the given room. @@ -381,4 +382,14 @@ internal interface RoomAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/upgrade") suspend fun upgradeRoom(@Path("roomId") roomId: String, @Body body: RoomUpgradeBody): RoomUpgradeResponse + + /** + * The API returns the summary of the specified room, if the room could be found and the client should be able to view + * its contents according to the join_rules, history visibility, space membership and similar rules outlined in MSC3173 + * as well as if the user is already a member of that room. + * https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "im.nheko.summary/rooms/{roomIdOrAlias}/summary") + suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String, + @Query("via") viaServers: List?): RoomStrippedState } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 794970705c..dbd0ae6f06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -253,4 +253,7 @@ internal abstract class RoomModule { @Binds abstract fun bindSign3pidInvitationTask(task: DefaultSign3pidInvitationTask): Sign3pidInvitationTask + + @Binds + abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt index 219e9c903f..63fc26e9d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFi import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.internal.session.room.GetRoomSummaryTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask @@ -49,6 +50,7 @@ internal class DefaultPeekRoomTask @Inject constructor( private val getRoomIdByAliasTask: GetRoomIdByAliasTask, private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, private val getPublicRoomTask: GetPublicRoomTask, + private val getRoomSummaryTask: GetRoomSummaryTask, private val resolveRoomStateTask: ResolveRoomStateTask ) : PeekRoomTask { @@ -70,6 +72,25 @@ internal class DefaultPeekRoomTask @Inject constructor( serverList = emptyList() } + // If the room summary API is available on the Home Server we should try it first + val strippedState = tryOrNull("Failed to get room stripped state roomId:$roomId") { + getRoomSummaryTask.execute(GetRoomSummaryTask.Params(roomId, serverList)) + } + if (strippedState != null) { + return PeekResult.Success( + roomId = strippedState.roomId, + alias = strippedState.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias }, + avatarUrl = strippedState.avatarUrl, + name = strippedState.name, + topic = strippedState.topic, + numJoinedMembers = strippedState.numJoinedMembers, + viaServers = serverList, + roomType = strippedState.roomType, + someMembers = null, + isPublic = strippedState.worldReadable + ) + } + // Is it a public room? val visibilityRes = tryOrNull("## PEEK: failed to get visibility") { getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId)) 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 89a3533946..4a6e27b7c0 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 @@ -23,6 +23,7 @@ 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.accountdata.RoomAccountDataTypes 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.RoomAliasesContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent @@ -31,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.RoomType 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.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -207,63 +209,102 @@ internal class RoomSummaryUpdater @Inject constructor( } .toMap() - lookupMap.keys.forEach { lookedUp -> - if (lookedUp.roomType == RoomType.SPACE) { - // get childrens + // First handle child relations + lookupMap.keys.asSequence() + .filter { it.roomType == RoomType.SPACE } + .forEach { lookedUp -> + // get childrens - lookedUp.children.clearWith { it.deleteFromRealm() } + lookedUp.children.clearWith { it.deleteFromRealm() } - RoomChildRelationInfo(realm, lookedUp.roomId).getDirectChildrenDescriptions().forEach { child -> + RoomChildRelationInfo(realm, lookedUp.roomId).getDirectChildrenDescriptions().forEach { child -> - lookedUp.children.add( - realm.createObject().apply { - this.childRoomId = child.roomId - this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst() - this.order = child.order + lookedUp.children.add( + realm.createObject().apply { + this.childRoomId = child.roomId + this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst() + this.order = child.order // this.autoJoin = child.autoJoin - this.viaServers.addAll(child.viaServers) - } - ) + this.viaServers.addAll(child.viaServers) + } + ) - RoomSummaryEntity.where(realm, child.roomId) - .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) - .findFirst() - ?.let { childSum -> - lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry -> - if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) { - // add looked up as a parent - entry.value.add(childSum) + RoomSummaryEntity.where(realm, child.roomId) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findFirst() + ?.let { childSum -> + lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry -> + if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) { + // add looked up as a parent + entry.value.add(childSum) + } } } + } + } + + // Now let's check parent relations + + lookupMap.keys + .forEach { lookedUp -> + lookedUp.parents.clearWith { it.deleteFromRealm() } + // can we check parent relations here?? + /** + * rooms can claim parents via the m.space.parent state event. + * canonical determines whether this is the main parent for the space. + * + * To avoid abuse where a room admin falsely claims that a room is part of a space that it should not be, + * clients could ignore such m.space.parent events unless either + * (a) there is a corresponding m.space.child event in the claimed parent, or + * (b) the sender of the m.space.child event has a sufficient power-level to send such an m.space.child event in the parent. + * (It is not necessarily required that that user currently be a member of the parent room - + * only the m.room.power_levels event is inspected.) + * [Checking the power-level rather than requiring an actual m.space.child event in the parent allows for "secret" rooms (see below).] + */ + RoomChildRelationInfo(realm, lookedUp.roomId).getParentDescriptions() + .map { parentInfo -> + // Is it a valid parent relation? + // Check if it's a child of the parent? + val isValidRelation: Boolean + val parent = lookupMap.firstNotNullOfOrNull { if (it.key.roomId == parentInfo.roomId) it.value else null } + if (parent?.firstOrNull { it.roomId == lookedUp.roomId } != null) { + // there is a corresponding m.space.child event in the claimed parent + isValidRelation = true + } else { + // check if sender can post child relation in parent? + val senderId = parentInfo.stateEventSender + val parentRoomId = parentInfo.roomId + val powerLevelsHelper = CurrentStateEventEntity + .getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_POWER_LEVELS) + ?.root + ?.let { ContentMapper.map(it.content).toModel() } + ?.let { PowerLevelsHelper(it) } + + isValidRelation = powerLevelsHelper?.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD) ?: false + } + + if (isValidRelation) { + lookedUp.parents.add( + realm.createObject().apply { + this.parentRoomId = parentInfo.roomId + this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst() + this.canonical = parentInfo.canonical + this.viaServers.addAll(parentInfo.viaServers) + } + ) + + RoomSummaryEntity.where(realm, parentInfo.roomId) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findFirst() + ?.let { parentSum -> + if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) { + // add lookedup as a parent + lookupMap[parentSum]?.add(lookedUp) + } + } + } } } - } else { - lookedUp.parents.clearWith { it.deleteFromRealm() } - // can we check parent relations here?? - RoomChildRelationInfo(realm, lookedUp.roomId).getParentDescriptions() - .map { parentInfo -> - - lookedUp.parents.add( - realm.createObject().apply { - this.parentRoomId = parentInfo.roomId - this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst() - this.canonical = parentInfo.canonical - this.viaServers.addAll(parentInfo.viaServers) - } - ) - - RoomSummaryEntity.where(realm, parentInfo.roomId) - .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) - .findFirst() - ?.let { parentSum -> - if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) { - // add lookedup as a parent - lookupMap[parentSum]?.add(lookedUp) - } - } - } - } - } // Simple algorithm to break cycles // Need more work to decide how to break, probably need to be as consistent as possible diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt index 8a6bbc18fd..8589db27b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -89,7 +89,6 @@ internal class DefaultSpace( body = SpaceChildContent( order = null, via = null, -// autoJoin = null, suggested = null ).toContent() ) 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 7be4cdcda9..ac20c79058 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 @@ -33,6 +33,7 @@ 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.create.CreateRoomPreset 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.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.JoinSpaceResult import org.matrix.android.sdk.api.session.space.Space @@ -77,7 +78,7 @@ internal class DefaultSpaceService @Inject constructor( if (isPublic) { this.roomAliasName = roomAliasLocalPart this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( - invite = 0 + invite = if (isPublic) Role.Default.value else Role.Moderator.value ) this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE @@ -221,4 +222,23 @@ internal class DefaultSpaceService @Inject constructor( ).toContent() ) } + + override suspend fun removeSpaceParent(childRoomId: String, parentSpaceId: String) { + val room = roomGetter.getRoom(childRoomId) + ?: throw IllegalArgumentException("Unknown Room $childRoomId") + + val existingEvent = room.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(parentSpaceId)) + if (existingEvent != null) { + // Should i check if it was sent by me? + // we don't check power level, it will throw if you cannot do that + room.sendStateEvent( + eventType = EventType.STATE_SPACE_PARENT, + stateKey = parentSpaceId, + body = SpaceParentContent( + via = null, + canonical = null + ).toContent() + ) + } + } } 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..df3d8492c3 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,9 @@ 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.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -26,7 +28,7 @@ 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 @@ -40,6 +42,8 @@ import java.io.File import java.net.SocketTimeoutException import javax.inject.Inject +private val loggerTag = LoggerTag("SyncTask", LoggerTag.SYNC) + internal interface SyncTask : Task { data class Params( @@ -53,7 +57,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, @@ -75,7 +79,7 @@ internal class DefaultSyncTask @Inject constructor( } private suspend fun doSync(params: SyncTask.Params) { - Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") + Timber.tag(loggerTag.value).d("Sync task started on Thread: ${Thread.currentThread().name}") val requestParams = HashMap() var timeout = 0L @@ -92,7 +96,7 @@ 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)) @@ -100,20 +104,20 @@ internal class DefaultSyncTask @Inject constructor( val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) 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) { + 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, @@ -122,43 +126,60 @@ 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) } } } - 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) + 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}") } 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 +189,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,9 +206,9 @@ 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 } } @@ -195,18 +216,18 @@ internal class DefaultSyncTask @Inject constructor( } private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) { - logDuration("INIT_SYNC handleSyncFile()") { - val syncResponse = logDuration("INIT_SYNC Read file and parse") { + 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) } 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..b3a6cafb7d 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.internal.session.call.ActiveCallHandler import org.matrix.android.sdk.internal.session.sync.SyncPresence @@ -49,6 +50,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, @@ -83,7 +86,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 +97,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 +105,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() @@ -124,21 +127,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 +151,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 +171,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) @@ -199,19 +202,19 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } 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 +228,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/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/multipicker/build.gradle b/multipicker/build.gradle index b2a5148b4a..437499df7b 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -19,14 +19,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' android { - compileSdkVersion 30 + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 19 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" - + minSdk versions.minSdk + targetSdk versions.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } @@ -41,11 +38,11 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation "androidx.fragment:fragment-ktx:1.3.6" - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation libs.jetbrains.kotlinStdlibJdk7 + implementation libs.androidx.appCompat + implementation libs.androidx.fragmentKtx + implementation libs.androidx.exifinterface // Log - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation libs.jakewharton.timber } 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 391140b9f3..4b0dd1f0a3 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,7 +162,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===105 +enum class===106 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/build.gradle b/vector/build.gradle index 9576ba86c0..7ef880730a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -13,8 +13,8 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 -ext.versionMinor = 2 -ext.versionPatch = 2 +ext.versionMinor = 3 +ext.versionPatch = 0 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -102,17 +102,20 @@ ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4]. def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 android { - compileSdkVersion 30 + + // Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use // Ref: https://issuetracker.google.com/issues/144111441 ndkVersion "21.3.6528147" + compileSdk versions.compileSdk + defaultConfig { applicationId "im.vector.app" // Set to API 21: see #405 - minSdkVersion 21 - targetSdkVersion 30 + minSdk versions.minSdk + targetSdk versions.targetSdk multiDexEnabled true renderscriptTargetApi 24 @@ -289,8 +292,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { @@ -313,26 +316,6 @@ android { dependencies { - def epoxy_version = '4.6.2' - def fragment_version = '1.3.6' - def arrow_version = "0.8.2" - def markwon_version = '4.1.2' - def big_image_viewer_version = '1.8.1' - def glide_version = '4.12.0' - def moshi_version = '1.12.0' - def daggerVersion = '2.38.1' - def autofill_version = "1.1.0" - def work_version = '2.5.0' - def arch_version = '2.1.0' - def lifecycle_version = '2.2.0' - def rxbinding_version = '3.1.0' - def jjwt_version = '0.11.2' - - // Tests - def kluent_version = '1.68' - def androidxTest_version = '1.4.0' - def espresso_version = '3.4.0' - implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") @@ -341,73 +324,77 @@ dependencies { implementation project(":library:ui-styles") implementation 'androidx.multidex:multidex:2.0.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + implementation libs.jetbrains.kotlinStdlibJdk7 + implementation libs.jetbrains.coroutinesCore + implementation libs.jetbrains.coroutinesAndroid - implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation libs.androidx.recyclerview + implementation libs.androidx.appCompat + implementation libs.androidx.fragmentKtx + implementation libs.androidx.constraintLayout implementation "androidx.sharetarget:sharetarget:1.1.0" - implementation 'androidx.core:core-ktx:1.6.0' - implementation "androidx.media:media:1.4.1" + implementation libs.androidx.core + implementation "androidx.media:media:1.4.2" implementation "androidx.transition:transition:1.4.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0" - implementation "com.squareup.moshi:moshi-adapters:$moshi_version" - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + implementation libs.squareup.moshi + kapt libs.squareup.moshiKotlin + implementation libs.androidx.lifecycleExtensions + implementation libs.androidx.lifecycleLivedata + + implementation libs.androidx.datastore + implementation libs.androidx.datastorepreferences + // Log - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation libs.jakewharton.timber // Debug implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.33' // rx - implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation libs.rx.rxKotlin + implementation libs.rx.rxAndroid implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1' // RXBinding - implementation "com.jakewharton.rxbinding3:rxbinding:$rxbinding_version" - implementation "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxbinding_version" - implementation "com.jakewharton.rxbinding3:rxbinding-material:$rxbinding_version" + implementation libs.jakewharton.rxbinding + implementation libs.jakewharton.rxbindingAppcompat + implementation libs.jakewharton.rxbindingMaterial - implementation("com.airbnb.android:epoxy:$epoxy_version") - implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version" - kapt "com.airbnb.android:epoxy-processor:$epoxy_version" - implementation "com.airbnb.android:epoxy-paging:$epoxy_version" - implementation 'com.airbnb.android:mvrx:1.5.1' + implementation libs.airbnb.epoxy + implementation libs.airbnb.epoxyGlide + kapt libs.airbnb.epoxyProcessor + implementation libs.airbnb.epoxyPaging + implementation libs.airbnb.mvrx // Work - implementation "androidx.work:work-runtime-ktx:$work_version" + implementation libs.androidx.work // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation libs.androidx.pagingRuntimeKtx // Functional Programming - implementation "io.arrow-kt:arrow-core:$arrow_version" + implementation libs.arrow.core // Pref - implementation 'androidx.preference:preference-ktx:1.1.1' + implementation libs.androidx.preferenceKtx // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.4.0' + implementation libs.google.material implementation 'me.gujun.android:span:1.7' - implementation "io.noties.markwon:core:$markwon_version" - implementation "io.noties.markwon:html:$markwon_version" + implementation libs.markwon.core + implementation libs.markwon.html implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.5.2' implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:2.0.1' - implementation "androidx.autofill:autofill:$autofill_version" + implementation libs.androidx.autoFill implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' implementation 'com.github.hyuwah:DraggableView:1.0.0' @@ -422,7 +409,7 @@ dependencies { // To convert voice message on old platforms implementation 'com.arthenica:ffmpeg-kit-audio:4.4.LTS' - //Alerter + // Alerter implementation 'com.tapadoo.android:alerter:7.0.1' implementation 'com.otaliastudios:autocomplete:1.1.0' @@ -431,16 +418,16 @@ dependencies { implementation 'com.squareup:seismic:1.0.2' // Image Loading - implementation "com.github.piasy:BigImageViewer:$big_image_viewer_version" - implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version" - implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version" - implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" + implementation libs.github.bigImageViewer + implementation libs.github.glideImageLoader + implementation libs.github.progressPieIndicator + implementation libs.github.glideImageViewFactory // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation "com.github.bumptech.glide:glide:$glide_version" - kapt "com.github.bumptech.glide:compiler:$glide_version" + implementation libs.github.glide + kapt libs.github.glideCompiler implementation 'com.danikula:videocache:2.7.1' implementation 'com.github.yalantis:ucrop:2.2.7' @@ -451,8 +438,8 @@ dependencies { implementation 'nl.dionsegijn:konfetti:1.3.2' implementation 'com.github.jetradarmobile:android-snowfall:1.2.1' // DI - implementation "com.google.dagger:dagger:$daggerVersion" - kapt "com.google.dagger:dagger-compiler:$daggerVersion" + implementation libs.dagger.dagger + kapt libs.dagger.daggerCompiler // gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:22.0.0') { @@ -492,36 +479,37 @@ dependencies { implementation 'im.dlg:android-dialer:1.2.5' // JWT - api "io.jsonwebtoken:jjwt-api:$jjwt_version" - runtimeOnly "io.jsonwebtoken:jjwt-impl:$jjwt_version" - runtimeOnly("io.jsonwebtoken:jjwt-orgjson:$jjwt_version") { + api libs.jsonwebtoken.jjwtApi + runtimeOnly libs.jsonwebtoken.jjwtImpl + runtimeOnly(libs.jsonwebtoken.jjwtOrgjson) { exclude group: 'org.json', module: 'json' //provided by Android natively } implementation 'commons-codec:commons-codec:1.15' // TESTS - testImplementation 'junit:junit:4.13.2' - testImplementation "org.amshove.kluent:kluent-android:$kluent_version" + testImplementation libs.tests.junit + testImplementation libs.tests.kluent // Plant Timber tree for test - testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + testImplementation libs.tests.timberJunitRule // Activate when you want to check for leaks, from time to time. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' - androidTestImplementation "androidx.test:core:$androidxTest_version" - androidTestImplementation "androidx.test:runner:$androidxTest_version" - androidTestImplementation "androidx.test:rules:$androidxTest_version" - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version" - androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version" - androidTestImplementation "androidx.arch.core:core-testing:$arch_version" + androidTestImplementation libs.androidx.testCore + androidTestImplementation libs.androidx.testRunner + androidTestImplementation libs.androidx.testRules + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espressoCore + androidTestImplementation libs.androidx.espressoContrib + androidTestImplementation libs.androidx.espressoIntents + androidTestImplementation libs.tests.kluent + androidTestImplementation libs.androidx.coreTesting // Plant Timber tree for test - androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + 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 a8eed30160..3fca617dee 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -47,6 +47,7 @@ + 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..aef5d3fe49 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(1_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/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 4cefeadb62..ddedfb93e3 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -39,17 +39,22 @@ import im.vector.app.features.notifications.NotifiableMessageEvent import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.SimpleNotifiableEvent +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import timber.log.Timber +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) + /** * Class extending FirebaseMessagingService. */ @@ -60,6 +65,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private lateinit var pusherManager: PushersManager private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences + private lateinit var vectorDataStore: VectorDataStore private lateinit var wifiDetector: WifiDetector private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -77,6 +83,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { pusherManager = pusherManager() activeSessionHolder = activeSessionHolder() vectorPreferences = vectorPreferences() + vectorDataStore = vectorDataStore() wifiDetector = wifiDetector() } } @@ -88,9 +95,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { */ override fun onMessageReceived(message: RemoteMessage) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + } + Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) + + runBlocking { + vectorDataStore.incrementPushCounter() } - Timber.d("## onMessageReceived() from FCM with priority %s", message.priority) // Diagnostic Push if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { @@ -100,14 +111,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (!vectorPreferences.areNotificationEnabledForDevice()) { - Timber.i("Notification are disabled for this device") + Timber.tag(loggerTag.value).i("Notification are disabled for this device") return } mUIHandler.post { if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // we are in foreground, let the sync do the things? - Timber.d("PUSH received in a foreground state, ignore") + Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { onMessageReceivedInternal(message.data) } @@ -121,7 +132,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * you retrieve the token. */ override fun onNewToken(refreshedToken: String) { - Timber.i("onNewToken: FCM Token has been updated") + Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") FcmHelper.storeFcmToken(this, refreshedToken) if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { pusherManager.registerPusherWithFcmKey(refreshedToken) @@ -138,7 +149,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * It is recommended that the app do a full sync with the app server after receiving this call. */ override fun onDeletedMessages() { - Timber.v("## onDeletedMessages()") + Timber.tag(loggerTag.value).v("## onDeletedMessages()") } /** @@ -150,9 +161,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun onMessageReceivedInternal(data: Map) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") } else { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } // update the badge counter @@ -162,24 +173,24 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val session = activeSessionHolder.getSafeActiveSession() if (session == null) { - Timber.w("## Can't sync from push, no current session") + Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { val eventId = data["event_id"] val roomId = data["room_id"] if (isEventAlreadyKnown(eventId, roomId)) { - Timber.d("Ignoring push, event already known") + Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster - Timber.d("Requesting event in fast lane") + Timber.tag(loggerTag.value).d("Requesting event in fast lane") getEventFastLane(session, roomId, eventId) - Timber.d("Requesting background sync") + Timber.tag(loggerTag.value).d("Requesting background sync") session.requireBackgroundSync() } } } catch (e: Exception) { - Timber.e(e, "## onMessageReceivedInternal() failed") + Timber.tag(loggerTag.value).e(e, "## onMessageReceivedInternal() failed") } } @@ -193,18 +204,18 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (wifiDetector.isConnectedToWifi().not()) { - Timber.d("No WiFi network, do not get Event") + Timber.tag(loggerTag.value).d("No WiFi network, do not get Event") return } coroutineScope.launch { - Timber.d("Fast lane: start request") + Timber.tag(loggerTag.value).d("Fast lane: start request") val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) resolvedEvent - ?.also { Timber.d("Fast lane: notify drawer") } + ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } ?.let { it.isPushGatewayEvent = true notificationDrawerManager.onNotifiableEventReceived(it) @@ -222,7 +233,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val room = session.getRoom(roomId) ?: return false return room.getTimeLineEvent(eventId) != null } catch (e: Exception) { - Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") + Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") } } return false @@ -230,7 +241,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) { if (session == null) { - Timber.e("## handleNotificationWithoutSyncingMode cannot find session") + Timber.tag(loggerTag.value).e("## handleNotificationWithoutSyncingMode cannot find session") return } @@ -263,9 +274,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) if (notifiableEvent == null) { - Timber.e("Unsupported notifiable event $eventId") + Timber.tag(loggerTag.value).e("Unsupported notifiable event $eventId") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.e("--> $event") + Timber.tag(loggerTag.value).e("--> $event") } } else { if (notifiableEvent is NotifiableMessageEvent) { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 5aec41e46f..6c9453a564 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -319,6 +319,7 @@ + { val height = measuredHeight return width to height } + +fun ImageView.setDrawableOrHide(drawableRes: Drawable?) { + setImageDrawable(drawableRes) + isVisible = drawableRes != null +} 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/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 5896122393..a27765bf4f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -61,6 +61,23 @@ class PushersManager @Inject constructor( ) } + fun registerEmailForPush(email: String) { + val currentSession = activeSessionHolder.getActiveSession() + val appName = appNameProvider.getAppName() + currentSession.addEmailPusher( + email = email, + lang = localeProvider.current().language, + emailBranding = appName, + appDisplayName = appName, + deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE" + ) + } + + suspend fun unregisterEmailPusher(email: String) { + val currentSession = activeSessionHolder.getSafeActiveSession() ?: return + currentSession.removeEmailPusher(email) + } + suspend fun unregisterPusher(pushKey: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index d8cf8cf6b8..cd3845f41b 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -50,7 +50,7 @@ private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP) class CallService : VectorService() { private val connections = mutableMapOf() - private val knownCalls = mutableSetOf() + private val knownCalls = mutableMapOf() private val connectedCallIds = mutableSetOf() private lateinit var notificationManager: NotificationManagerCompat @@ -190,7 +190,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleCallTerminated(intent: Intent) { @@ -198,20 +198,22 @@ class CallService : VectorService() { val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false) alertManager.cancelAlert(callId) - val terminatedCall = knownCalls.firstOrNull { it.callId == callId } + val terminatedCall = knownCalls.remove(callId) if (terminatedCall == null) { - Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$") + Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId") handleUnexpectedState(callId) return } - knownCalls.remove(terminatedCall) + val notification = notificationUtils.buildCallEndedNotification(false) + val notificationId = callId.hashCode() + startForeground(notificationId, notification) if (knownCalls.isEmpty()) { + Timber.tag(loggerTag.value).v("No more call, stop the service") + stopForeground(true) mediaSession?.isActive = false myStopSelf() } val wasConnected = connectedCallIds.remove(callId) - val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall) - notificationManager.notify(callId.hashCode(), notification) if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) { val missedCallNotification = notificationUtils.buildCallMissedNotification(terminatedCall) notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), missedCallNotification) @@ -243,7 +245,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } /** @@ -267,18 +269,19 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleUnexpectedState(callId: String?) { Timber.tag(loggerTag.value).v("Fallback to clear everything") callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() - if (callId != null) { - notificationManager.cancel(callId.hashCode()) - } val notification = notificationUtils.buildCallEndedNotification(false) - startForeground(DEFAULT_NOTIFICATION_ID, notification) + if (callId != null) { + startForeground(callId.hashCode(), notification) + } else { + startForeground(DEFAULT_NOTIFICATION_ID, notification) + } if (knownCalls.isEmpty()) { mediaSession?.isActive = false myStopSelf() @@ -371,7 +374,7 @@ class CallService : VectorService() { putExtra(EXTRA_END_CALL_REASON, endCallReason) putExtra(EXTRA_END_CALL_REJECTED, rejected) } - ContextCompat.startForegroundService(context, intent) + context.startService(intent) } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index 97194f2c03..a3e8b3780c 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -26,6 +26,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.setDrawableOrHide import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewBottomSheetActionButtonBinding import im.vector.app.features.themes.ThemeUtils @@ -80,7 +81,7 @@ class BottomSheetActionButton @JvmOverloads constructor( var rightIcon: Drawable? = null set(value) { field = value - views.bottomSheetActionIcon.setImageDrawable(value) + views.bottomSheetActionIcon.setDrawableOrHide(value) } var tint: Int? = null @@ -95,6 +96,12 @@ class BottomSheetActionButton @JvmOverloads constructor( value?.let { views.bottomSheetActionTitle.setTextColor(it) } } + var isBetaAction: Boolean? = null + set(value) { + field = value + views.bottomSheetActionBeta.isVisible = field ?: false + } + init { inflate(context, R.layout.view_bottom_sheet_action_button, this) views = ViewBottomSheetActionButtonBinding.bind(this) @@ -109,6 +116,8 @@ class BottomSheetActionButton @JvmOverloads constructor( tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ThemeUtils.getColor(context, R.attr.colorPrimary)) + + isBetaAction = getBoolean(R.styleable.BottomSheetActionButton_betaAction, false) } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/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/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 7806f2603d..c73fa70388 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -20,6 +20,7 @@ import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R /** * Open a web view above the current activity. @@ -38,3 +39,14 @@ fun Context.displayInWebView(url: String) { .setPositiveButton(android.R.string.ok, null) .show() } + +fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, consentCallBack: (() -> Unit)) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, configuredIdentityServer ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + consentCallBack.invoke() + } + .setNegativeButton(R.string.no, null) + .show() +} diff --git a/vector/src/main/java/im/vector/app/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/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/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 63ba83bdbc..90df595f8f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -134,7 +134,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 +167,7 @@ class VectorCallViewModel @AssistedInject constructor( } private fun updateOtherKnownCall(currentCall: WebRtcCall) { - val otherCall = callManager.getCalls().firstOrNull { - it.callId != currentCall.callId && it.mxCall.state is CallState.Connected - } + val otherCall = getOtherKnownCall(currentCall) setState { if (otherCall == null) { copy(otherKnownCallInfo = null) @@ -171,6 +177,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 +196,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 +242,7 @@ class VectorCallViewModel @AssistedInject constructor( } override fun onCleared() { - callManager.removeCurrentCallListener(currentCallListener) + callManager.removeListener(callManagerListener) call?.removeListener(callListener) call = null proximityManager.stop() @@ -310,10 +322,10 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } - VectorCallViewActions.TransferCall -> { + VectorCallViewActions.TransferCall -> { handleCallTransfer() } - is VectorCallViewActions.SwitchCall -> { + is VectorCallViewActions.SwitchCall -> { setState { VectorCallViewState(action.callArgs) } setupCallWithCurrentState() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt similarity index 63% rename from vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt rename to vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt index 00ad7c540e..cfa076f31b 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt @@ -28,20 +28,21 @@ import com.facebook.react.bridge.JavaOnlyMap import org.jitsi.meet.sdk.BroadcastEmitter import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.JitsiMeet -import org.matrix.android.sdk.api.extensions.tryOrNull +import timber.log.Timber private const val CONFERENCE_URL_DATA_KEY = "url" -fun BroadcastEvent.extractConferenceUrl(): String? { - return when (type) { - BroadcastEvent.Type.CONFERENCE_TERMINATED, - BroadcastEvent.Type.CONFERENCE_WILL_JOIN, - BroadcastEvent.Type.CONFERENCE_JOINED -> data[CONFERENCE_URL_DATA_KEY] as? String - else -> null +sealed class ConferenceEvent(open val data: Map) { + data class Terminated(override val data: Map) : ConferenceEvent(data) + data class WillJoin(override val data: Map) : ConferenceEvent(data) + data class Joined(override val data: Map) : ConferenceEvent(data) + + fun extractConferenceUrl(): String? { + return data[CONFERENCE_URL_DATA_KEY] as? String } } -class JitsiBroadcastEmitter(private val context: Context) { +class ConferenceEventEmitter(private val context: Context) { fun emitConferenceEnded() { val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference()) @@ -49,8 +50,9 @@ class JitsiBroadcastEmitter(private val context: Context) { } } -class JitsiBroadcastEventObserver(private val context: Context, - private val onBroadcastEvent: (BroadcastEvent) -> Unit) : LifecycleObserver { +class ConferenceEventObserver(private val context: Context, + private val onBroadcastEvent: (ConferenceEvent) -> Unit) + : LifecycleObserver { // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events private val broadcastReceiver = object : BroadcastReceiver() { @@ -61,8 +63,10 @@ class JitsiBroadcastEventObserver(private val context: Context, @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun unregisterForBroadcastMessages() { - tryOrNull("Unable to unregister receiver") { + try { LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver) + } catch (throwable: Throwable) { + Timber.v("Unable to unregister receiver") } } @@ -72,13 +76,23 @@ class JitsiBroadcastEventObserver(private val context: Context, for (type in BroadcastEvent.Type.values()) { intentFilter.addAction(type.action) } - tryOrNull("Unable to register receiver") { + try { LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter) + } catch (throwable: Throwable) { + Timber.v("Unable to register receiver") } } private fun onBroadcastReceived(intent: Intent) { val event = BroadcastEvent(intent) - onBroadcastEvent(event) + val conferenceEvent = when (event.type) { + BroadcastEvent.Type.CONFERENCE_JOINED -> ConferenceEvent.Joined(event.data) + BroadcastEvent.Type.CONFERENCE_TERMINATED -> ConferenceEvent.Terminated(event.data) + BroadcastEvent.Type.CONFERENCE_WILL_JOIN -> ConferenceEvent.WillJoin(event.data) + else -> null + } + if (conferenceEvent != null) { + onBroadcastEvent(conferenceEvent) + } } } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt index 1a9fc5ea10..179956612d 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt @@ -18,7 +18,6 @@ package im.vector.app.features.call.conference import android.content.Context import androidx.lifecycle.ProcessLifecycleOwner -import org.jitsi.meet.sdk.BroadcastEvent import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject import javax.inject.Singleton @@ -29,18 +28,18 @@ class JitsiActiveConferenceHolder @Inject constructor(context: Context) { private var activeConference: String? = null init { - ProcessLifecycleOwner.get().lifecycle.addObserver(JitsiBroadcastEventObserver(context, this::onBroadcastEvent)) + ProcessLifecycleOwner.get().lifecycle.addObserver(ConferenceEventObserver(context, this::onBroadcastEvent)) } fun isJoined(confId: String?): Boolean { return confId != null && activeConference?.endsWith(confId).orFalse() } - private fun onBroadcastEvent(broadcastEvent: BroadcastEvent) { - when (broadcastEvent.type) { - BroadcastEvent.Type.CONFERENCE_JOINED -> activeConference = broadcastEvent.extractConferenceUrl() - BroadcastEvent.Type.CONFERENCE_TERMINATED -> activeConference = null - else -> Unit + private fun onBroadcastEvent(conferenceEvent: ConferenceEvent) { + when (conferenceEvent) { + is ConferenceEvent.Joined -> activeConference = conferenceEvent.extractConferenceUrl() + is ConferenceEvent.Terminated -> activeConference = null + else -> Unit } } } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index a7a6f99cfc..e7fd541f3d 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -39,13 +39,13 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityJitsiBinding import kotlinx.parcelize.Parcelize -import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.JitsiMeet import org.jitsi.meet.sdk.JitsiMeetActivityDelegate import org.jitsi.meet.sdk.JitsiMeetActivityInterface import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetView import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.JsonDict import timber.log.Timber import java.net.URL import javax.inject.Inject @@ -87,7 +87,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee JitsiCallViewEvents.LeaveConference -> handleLeaveConference() }.exhaustive } - lifecycle.addObserver(JitsiBroadcastEventObserver(this, this::onBroadcastEvent)) + lifecycle.addObserver(ConferenceEventObserver(this, this::onBroadcastEvent)) } override fun onResume() { @@ -113,7 +113,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee jitsiMeetView?.dispose() // Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen). if (currentConf != null) { - JitsiBroadcastEmitter(this).emitConferenceEnded() + ConferenceEventEmitter(this).emitConferenceEnded() } JitsiMeetActivityDelegate.onHostDestroy(this) super.onDestroy() @@ -223,15 +223,15 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults) } - private fun onBroadcastEvent(event: BroadcastEvent) { - Timber.v("Broadcast received: ${event.type}") - when (event.type) { - BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data) - else -> Unit + private fun onBroadcastEvent(event: ConferenceEvent) { + Timber.v("Broadcast received: $event") + when (event) { + is ConferenceEvent.Terminated -> onConferenceTerminated(event.data) + else -> Unit } } - private fun onConferenceTerminated(data: Map) { + private fun onConferenceTerminated(data: JsonDict) { Timber.v("JitsiMeetViewListener.onConferenceTerminated()") // Do not finish if there is an error if (data["error"] == null) { diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt index e7c8602698..3472d01c72 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt @@ -89,6 +89,10 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment() + private val currentCallsListeners = CopyOnWriteArrayList() - fun addCurrentCallListener(listener: CurrentCallListener) { + fun addListener(listener: Listener) { currentCallsListeners.add(listener) } - fun removeCurrentCallListener(listener: CurrentCallListener) { + fun removeListener(listener: Listener) { currentCallsListeners.remove(listener) } @@ -250,10 +251,13 @@ class WebRtcCallManager @Inject constructor( callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) transferees.remove(callId) - if (getCurrentCall()?.callId == callId) { + if (currentCall.get()?.callId == callId) { val otherCall = getCalls().lastOrNull() currentCall.setAndNotify(otherCall) } + tryOrNull { + currentCallsListeners.forEach { it.onCallEnded(callId) } + } // There is no active calls if (getCurrentCall() == null) { Timber.tag(loggerTag.value).v("Dispose peerConnectionFactory as there is no need to keep one") @@ -424,7 +428,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId") - onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false) + val call = callsByCallId[callId] + ?: return Unit.also { + Timber.tag(loggerTag.value).w("onCallManagedByOtherSession for non active call? $callId") + } + call.endCall(EndCallReason.ANSWERED_ELSEWHERE, sendSignaling = false) } override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 3719618d31..206c5af17a 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -48,7 +48,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CONFETTI("/confetti", "", R.string.command_confetti, false), SNOWFALL("/snowfall", "", R.string.command_snow, false), CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 9291352821..19024fcf8b 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -23,14 +23,13 @@ import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.widget.checkedChanges import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentContactsBookBinding import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.UserListAction @@ -76,14 +75,9 @@ class ContactsBookFragment @Inject constructor( private fun setupConsentView() { views.phoneBookSearchForMatrixContacts.setOnClickListener { withState(contactsBookViewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) - .setPositiveButton(R.string.yes) { _, _ -> - contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog(state.identityServerUrl) { + contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 0b8674ec6f..41b83c627d 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.ensureProtocol +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.discovery.change.SetIdentityServerFragment import im.vector.app.features.settings.VectorSettingsActivity @@ -179,14 +180,9 @@ class DiscoverySettingsFragment @Inject constructor( override fun onTapUpdateUserConsent(newValue: Boolean) { if (newValue) { withState(viewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke())) - .setPositiveButton(R.string.yes) { _, _ -> - viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog(state.identityServer.invoke()) { + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) + } } } else { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false)) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 11fd796534..3cd6c31ab2 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -65,7 +65,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( setState { copy( identityServer = Success(identityServerUrl), - userConsent = false + userConsent = identityService.getUserConsent() ) } if (currentIS != identityServerUrl) retrieveBinding() diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index d22707bda0..2ee3233637 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -138,13 +138,13 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active target: Target) { val placeholder = getPlaceholderDrawable(matrixItem) glideRequests.loadResolvedUrl(matrixItem.avatarUrl) - .apply { + .let { when (matrixItem) { is MatrixItem.SpaceItem -> { - transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) + it.transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) } else -> { - apply(RequestOptions.circleCropTransform()) + it.apply(RequestOptions.circleCropTransform()) } } } @@ -157,13 +157,13 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { return glideRequests .asBitmap() - .apply { + .let { val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) if (resolvedUrl != null) { - load(resolvedUrl) + it.load(resolvedUrl) } else { val avatarColor = matrixItemColorProvider.getColor(matrixItem) - load(TextDrawable.builder() + it.load(TextDrawable.builder() .beginConfig() .bold() .endConfig() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index a302276f45..4b12237bdf 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -70,7 +70,7 @@ import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.push.fcm.FcmHelper import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy @@ -81,7 +81,8 @@ import javax.inject.Inject @Parcelize data class HomeActivityArgs( val clearNotification: Boolean, - val accountCreation: Boolean + val accountCreation: Boolean, + val inviteNotificationRoomId: String? = null ) : Parcelable class HomeActivity : @@ -229,6 +230,11 @@ class HomeActivity : if (args?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (args?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(args.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } homeActivityViewModel.observeViewEvents { when (it) { @@ -302,11 +308,11 @@ class HomeActivity : } private fun renderState(state: HomeActivityViewState) { - when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + when (val status = state.syncStatusServiceStatus) { + is SyncStatusService.Status.Idle -> { views.waitingView.root.isVisible = false } - is InitialSyncProgressService.Status.Progressing -> { + is SyncStatusService.Status.Progressing -> { val initSyncStepStr = initSyncStepFormatter.format(status.initSyncStep) Timber.v("$initSyncStepStr ${status.percentProgress}") views.waitingView.root.setOnClickListener { @@ -324,6 +330,7 @@ class HomeActivity : } views.waitingView.root.isVisible = true } + else -> Unit }.exhaustive } @@ -422,9 +429,17 @@ class HomeActivity : override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { + val parcelableExtra = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (parcelableExtra?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (parcelableExtra?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession() + ?.permalinkService() + ?.createPermalink(parcelableExtra.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } handleIntent(intent) } @@ -460,8 +475,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) } @@ -548,10 +563,15 @@ class HomeActivity : } companion object { - fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { + fun newIntent(context: Context, + clearNotification: Boolean = false, + accountCreation: Boolean = false, + inviteNotificationRoomId: String? = null + ): Intent { val args = HomeActivityArgs( clearNotification = clearNotification, - accountCreation = accountCreation + accountCreation = accountCreation, + inviteNotificationRoomId = inviteNotificationRoomId ) return Intent(context, HomeActivity::class.java) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index bfedbd6f52..1aa2f59337 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem @@ -122,25 +122,26 @@ class HomeActivityViewModel @AssistedInject constructor( private fun observeInitialSync() { val session = activeSessionHolder.getSafeActiveSession() ?: return - session.getInitialSyncProgressStatus() + session.getSyncStatusLive() .asObservable() .subscribe { status -> when (status) { - is InitialSyncProgressService.Status.Progressing -> { + is SyncStatusService.Status.Progressing -> { // Schedule a check of the bootstrap when the init sync will be finished checkBootstrap = true } - is InitialSyncProgressService.Status.Idle -> { + is SyncStatusService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false maybeBootstrapCrossSigningAfterInitialSync() } } + else -> Unit } setState { copy( - initialSyncProgressServiceStatus = status + syncStatusServiceStatus = status ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index d4df7cd073..f3bddaf0d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -17,8 +17,8 @@ package im.vector.app.features.home import com.airbnb.mvrx.MvRxState -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService data class HomeActivityViewState( - val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle + val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 9b71d1c90c..627f4b4581 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -29,6 +29,7 @@ import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.badge.BadgeDrawable +import im.vector.app.AppStateHandler import im.vector.app.R import im.vector.app.RoomGroupingMethod import im.vector.app.core.extensions.commitTransaction @@ -69,7 +70,8 @@ class HomeDetailFragment @Inject constructor( private val colorProvider: ColorProvider, private val alertManager: PopupAlertManager, private val callManager: WebRtcCallManager, - private val vectorPreferences: VectorPreferences + private val vectorPreferences: VectorPreferences, + private val appStateHandler: AppStateHandler ) : VectorBaseFragment(), KeysBackupBanner.Delegate, CurrentCallsView.Callback, @@ -204,6 +206,18 @@ class HomeDetailFragment @Inject constructor( // update notification tab if needed updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab()) callManager.checkForProtocolsSupportIfNeeded() + + // Current space/group is not live so at least refresh toolbar on resume + appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod -> + when (roomGroupingMethod) { + is RoomGroupingMethod.ByLegacyGroup -> { + onGroupChange(roomGroupingMethod.groupSummary) + } + is RoomGroupingMethod.BySpace -> { + onSpaceChange(roomGroupingMethod.spaceSummary) + } + } + } } private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { @@ -426,7 +440,11 @@ class HomeDetailFragment @Inject constructor( views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) - views.syncStateView.render(it.syncState) + views.syncStateView.render( + it.syncState, + it.incrementalSyncStatus, + it.pushCounter, + vectorPreferences.developerShowDebugInfo()) hasUnreadRooms = it.hasUnreadMessages } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index b960402f90..460975c2c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -33,13 +33,16 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams @@ -56,10 +59,11 @@ import java.util.concurrent.TimeUnit class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, + private val vectorDataStore: VectorDataStore, private val callManager: WebRtcCallManager, private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler, -private val autoAcceptInvites: AutoAcceptInvites) + private val autoAcceptInvites: AutoAcceptInvites) : VectorViewModel(initialState), CallProtocolsChecker.Listener { @@ -89,6 +93,7 @@ private val autoAcceptInvites: AutoAcceptInvites) observeRoomGroupingMethod() observeRoomSummaries() updateShowDialPadTab() + observeDataStore() callManager.addProtocolsCheckerListener(this) session.rx().liveUser(session.myUserId).execute { copy( @@ -97,6 +102,18 @@ private val autoAcceptInvites: AutoAcceptInvites) } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + override fun handle(action: HomeDetailAction) { when (action) { is HomeDetailAction.SwitchTab -> handleSwitchTab(action) @@ -173,6 +190,17 @@ private val autoAcceptInvites: AutoAcceptInvites) } } .disposeOnClear() + + session.getSyncStatusLive() + .asObservable() + .subscribe { + if (it is SyncStatusService.Status.IncrementalSyncStatus) { + setState { + copy(incrementalSyncStatus = it) + } + } + } + .disposeOnClear() } private fun observeRoomGroupingMethod() { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 304444abdd..4022a0d9fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.RoomGroupingMethod +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.util.MatrixItem @@ -39,6 +40,8 @@ data class HomeDetailViewState( val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val showDialPadTab: Boolean = false ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt index f02711690a..ac983a9f0c 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.rx.asObservable import java.util.concurrent.TimeUnit @@ -143,6 +144,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 +173,16 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia // filter out current selection it.roomId != selectedSpace } + CountInfo( homeCount = counts, otherCount = RoomAggregateNotificationCount( - rootCounts.fold(0, { acc, rs -> - acc + rs.notificationCount - }) + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0), - rootCounts.fold(0, { acc, rs -> - acc + rs.highlightCount - }) + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount, + highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 94388dcfeb..9bb82cdc27 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction -import org.jitsi.meet.sdk.BroadcastEvent +import im.vector.app.features.call.conference.ConferenceEvent import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -97,7 +97,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class EnsureNativeWidgetAllowed(val widget: Widget, val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() - data class UpdateJoinJitsiCallStatus(val jitsiEvent: BroadcastEvent): RoomDetailAction() + data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent): RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index d45aa69cf3..c6eda584ad 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -86,6 +86,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 @@ -123,8 +124,9 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.conference.JitsiBroadcastEmitter -import im.vector.app.features.call.conference.JitsiBroadcastEventObserver +import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.call.conference.ConferenceEventEmitter +import im.vector.app.features.call.conference.ConferenceEventObserver import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.Command @@ -152,6 +154,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet +import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -184,7 +187,6 @@ import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser -import org.jitsi.meet.sdk.BroadcastEvent import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData @@ -312,7 +314,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) @@ -324,7 +329,7 @@ class RoomDetailFragment @Inject constructor( } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - lifecycle.addObserver(JitsiBroadcastEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) + lifecycle.addObserver(ConferenceEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) @@ -340,16 +345,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() @@ -366,10 +370,10 @@ class RoomDetailFragment @Inject constructor( knownCallsViewModel .liveKnownCalls - .observe(viewLifecycleOwner, { + .observe(viewLifecycleOwner) { currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() - }) + } roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> updateJumpToReadMarkerViewVisibility() @@ -387,8 +391,17 @@ class RoomDetailFragment @Inject constructor( } } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> - views.syncStateView.render(syncState) + roomDetailViewModel.selectSubscribe( + RoomDetailViewState::syncState, + RoomDetailViewState::incrementalSyncStatus, + RoomDetailViewState::pushCounter + ) { syncState, incrementalSyncStatus, pushCounter -> + views.syncStateView.render( + syncState, + incrementalSyncStatus, + pushCounter, + vectorPreferences.developerShowDebugInfo() + ) } roomDetailViewModel.observeViewEvents { @@ -454,11 +467,11 @@ class RoomDetailFragment @Inject constructor( } private fun leaveJitsiConference() { - JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded() + ConferenceEventEmitter(vectorBaseActivity).emitConferenceEnded() } - private fun onBroadcastJitsiEvent(jitsiEvent: BroadcastEvent) { - roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(jitsiEvent)) + private fun onBroadcastJitsiEvent(conferenceEvent: ConferenceEvent) { + roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(conferenceEvent)) } private fun onCannotRecord() { @@ -584,8 +597,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) @@ -602,14 +621,18 @@ class RoomDetailFragment @Inject constructor( } } .build(views.composerLayout.views.composerEditText) + } - views.composerLayout.views.composerEmojiButton.debouncedClicks { - emojiPopup.toggle() + private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + // In this case, let the user start again the gesture + } else if (deniedPermanently) { + vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) } } - private fun setupFailedMessagesWarningView() { - views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback { + private fun createFailedMessagesWarningCallback(): FailedMessagesWarningView.Callback { + return object : FailedMessagesWarningView.Callback { override fun onDeleteAllClicked() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.event_status_delete_all_failed_dialog_title) @@ -627,14 +650,6 @@ class RoomDetailFragment @Inject constructor( } } - private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted) { - // In this case, let the user start again the gesture - } else if (deniedPermanently) { - vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) - } - } - private fun setupVoiceMessageView() { views.voiceMessageRecorderView.voiceMessagePlaybackTracker = voiceMessagePlaybackTracker @@ -767,6 +782,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroyView() { + lazyLoadedViews.unBind() timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) currentCallsViewPresenter.unBind() @@ -774,8 +790,6 @@ class RoomDetailFragment @Inject constructor( autoCompleter.clear() debouncer.cancelAll() views.timelineRecyclerView.cleanup() - emojiPopup.dismiss() - super.onDestroyView() } @@ -1350,22 +1364,22 @@ class RoomDetailFragment @Inject constructor( return isHandled } - private fun setupInviteView() { - views.inviteView.callback = this - } - override fun invalidate() = withState(roomDetailViewModel) { state -> invalidateOptionsMenu() val summary = state.asyncRoomSummary() renderToolbar(summary, state.typingMessage) views.removeJitsiWidgetView.render(state) - views.failedMessagesWarningView.render(state.hasFailedSending) + if (state.hasFailedSending) { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true + } else { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false + } val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages timelineEventController.update(state) - views.inviteView.isVisible = false + lazyLoadedViews.inviteView(false)?.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { @@ -1386,10 +1400,15 @@ class RoomDetailFragment @Inject constructor( views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.inviteView.isVisible = true - views.inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) - // Intercept click event - views.inviteView.setOnClickListener { } + views.composerLayout.isVisible = false + views.voiceMessageRecorderView.isVisible = false + lazyLoadedViews.inviteView(true)?.apply { + callback = this@RoomDetailFragment + isVisible = true + render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) + setOnClickListener { } + } + Unit } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } @@ -1713,7 +1732,7 @@ class RoomDetailFragment @Inject constructor( override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false + val roomId = roomDetailArgs.roomId this.view?.hideKeyboard() MessageActionsBottomSheet diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 3e902dc2ef..cacf9b8902 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -38,8 +38,9 @@ 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 @@ -56,17 +57,18 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoicePlayerHelper import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer -import org.jitsi.meet.sdk.BroadcastEvent import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull @@ -80,6 +82,7 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership @@ -102,6 +105,7 @@ import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber @@ -111,6 +115,7 @@ 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, @@ -174,6 +179,7 @@ class RoomDetailViewModel @AssistedInject constructor( observeSummaryState() getUnreadState() observeSyncState() + observeDataStore() observeEventDisplayedActions() loadDraftIfAny() observeUnreadState() @@ -198,6 +204,18 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + private fun prepareForEncryption() { // check if there is not already a call made, or if there has been an error if (prepareToEncrypt.shouldLoad) { @@ -368,12 +386,12 @@ class RoomDetailViewModel @AssistedInject constructor( } return@withState } - when (action.jitsiEvent.type) { - BroadcastEvent.Type.CONFERENCE_JOINED, - BroadcastEvent.Type.CONFERENCE_TERMINATED -> { + when (action.conferenceEvent) { + is ConferenceEvent.Joined, + is ConferenceEvent.Terminated -> { setState { copy(jitsiState = jitsiState.copy(hasJoined = activeConferenceHolder.isJoined(jitsiState.confId))) } } - else -> Unit + else -> Unit } } @@ -1493,6 +1511,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } .disposeOnClear() + + session.getSyncStatusLive() + .asObservable() + .subscribe { it -> + if (it is SyncStatusService.Status.IncrementalSyncStatus) { + setState { + copy(incrementalSyncStatus = it) + } + } + } + .disposeOnClear() } private fun observeRoomSummary() { 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..8f4ad97b72 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -77,6 +78,8 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, val canShowJumpToReadMarker: Boolean = true, 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 8be319f2a8..39b3cd5061 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -39,13 +39,13 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams -import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem @@ -276,6 +276,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { + // Don't build anything if membership is not joined + if (partialState.roomSummary?.membership != Membership.JOIN) { + return + } val timestamp = System.currentTimeMillis() val showingForwardLoader = LoadingItem_() 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/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 733eb0575e..26df42dbe1 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -70,7 +70,6 @@ class MatrixToRoomSpaceFragment @Inject constructor( val matrixItem = peek.roomItem avatarRenderer.render(matrixItem, views.matrixToCardAvatar) if (peek.roomType == RoomType.SPACE) { - views.matrixToBetaTag.isVisible = true views.matrixToAccessImage.isVisible = true if (peek.isPublic) { views.matrixToAccessText.setTextOrHide(context?.getString(R.string.public_space)) @@ -79,8 +78,6 @@ class MatrixToRoomSpaceFragment @Inject constructor( views.matrixToAccessText.setTextOrHide(context?.getString(R.string.private_space)) views.matrixToAccessImage.setImageResource(R.drawable.ic_room_private) } - } else { - views.matrixToBetaTag.isVisible = false } views.matrixToCardNameText.setTextOrHide(peek.name) views.matrixToCardAliasText.setTextOrHide(peek.alias) diff --git a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt index 00d107513c..c56481d3f2 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt @@ -50,7 +50,6 @@ class SpaceCardRenderer @Inject constructor( inCard.matrixToCardButtonLoading.isVisible = false avatarRenderer.render(spaceSummary.toMatrixItem(), inCard.matrixToCardAvatar) inCard.matrixToCardNameText.text = spaceSummary.name - inCard.matrixToBetaTag.isVisible = true inCard.matrixToCardAliasText.setTextOrHide(spaceSummary.canonicalAlias) inCard.matrixToCardDescText.setTextOrHide(spaceSummary.topic.linkify(matrixLinkCallback)) if (spaceSummary.isPublic) { @@ -97,7 +96,6 @@ class SpaceCardRenderer @Inject constructor( inCard.matrixToCardButtonLoading.isVisible = false avatarRenderer.render(spaceChildInfo.toMatrixItem(), inCard.matrixToCardAvatar) inCard.matrixToCardNameText.setTextOrHide(spaceChildInfo.name) - inCard.matrixToBetaTag.isVisible = true inCard.matrixToCardAliasText.setTextOrHide(spaceChildInfo.canonicalAlias) inCard.matrixToCardDescText.setTextOrHide(spaceChildInfo.topic?.linkify(matrixLinkCallback)) if (spaceChildInfo.worldReadable) { 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..c575063ce9 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 @@ -468,7 +468,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) @@ -679,7 +678,7 @@ 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) 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..822f291e1f 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 @@ -41,15 +41,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..7140bb0baa 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,6 +25,7 @@ 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 @@ -36,6 +37,7 @@ 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 @@ -91,17 +93,19 @@ 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) diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 45d26e81eb..d64ee0f705 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.reactions +import android.annotation.SuppressLint import android.os.Build import android.os.Trace import android.text.Layout @@ -30,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R -import im.vector.app.features.reactions.data.EmojiDataSource +import im.vector.app.features.reactions.data.EmojiData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -43,13 +44,13 @@ import kotlin.math.abs * TODO: Performances * TODO: Scroll to section - Find a way to snap section to the top */ -class EmojiRecyclerAdapter @Inject constructor( - private val dataSource: EmojiDataSource -) : +class EmojiRecyclerAdapter @Inject constructor() : RecyclerView.Adapter() { 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/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt index ac7aee797a..8e12dd2cca 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,17 +15,19 @@ */ package im.vector.app.features.reactions +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory 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.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 = "", @@ -58,11 +60,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 d81767cc7f..0f6f1620a8 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 @@ -15,27 +15,18 @@ */ package im.vector.app.features.reactions.widget -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator import android.content.Context -import android.content.res.TypedArray import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.view.Gravity import android.view.View -import android.view.animation.AccelerateDecelerateInterpolator -import android.view.animation.DecelerateInterpolator -import android.view.animation.OvershootInterpolator -import android.widget.ImageView -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.constraintlayout.widget.ConstraintLayout +import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes 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 import javax.inject.Inject @@ -47,7 +38,7 @@ import javax.inject.Inject class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) - : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { + : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { init { if (context is HasScreenInjector) { @@ -55,21 +46,11 @@ class ReactionButton @JvmOverloads constructor(context: Context, } } - companion object { - private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() - private val ACCELERATE_DECELERATE_INTERPOLATOR = AccelerateDecelerateInterpolator() - private val OVERSHOOT_INTERPOLATOR = OvershootInterpolator(4f) - } - @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper private val views: ReactionButtonBinding var reactedListener: ReactedListener? = null - private var dotPrimaryColor: Int = 0 - private var dotSecondaryColor: Int = 0 - private var circleStartColor: Int = 0 - private var circleEndColor: Int = 0 var reactionCount = 11 set(value) { @@ -85,50 +66,24 @@ class ReactionButton @JvmOverloads constructor(context: Context, views.reactionText.text = emojiSpanned } - private var animationScaleFactor: Float = 0.toFloat() - private var isChecked: Boolean = false - - private var animatorSet: AnimatorSet? = null - private var onDrawable: Drawable? = null private var offDrawable: Drawable? = null init { inflate(context, R.layout.reaction_button, this) + orientation = HORIZONTAL + minimumHeight = DimensionConverter(context.resources).dpToPx(30) + gravity = Gravity.CENTER views = ReactionButtonBinding.bind(this) views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount) - -// emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) { onDrawable = ContextCompat.getDrawable(context, R.drawable.reaction_rounded_rect_shape) offDrawable = ContextCompat.getDrawable(context, R.drawable.reaction_rounded_rect_shape_off) - - circleStartColor = getColor(R.styleable.ReactionButton_circle_start_color, 0) - - if (circleStartColor != 0) { - views.circle.startColor = circleStartColor - } - - circleEndColor = getColor(R.styleable.ReactionButton_circle_end_color, 0) - - if (circleEndColor != 0) { - views.circle.endColor = circleEndColor - } - - dotPrimaryColor = getColor(R.styleable.ReactionButton_dots_primary_color, 0) - dotSecondaryColor = getColor(R.styleable.ReactionButton_dots_secondary_color, 0) - - if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { - views.dots.setColors(dotPrimaryColor, dotSecondaryColor) - } - getString(R.styleable.ReactionButton_emoji)?.let { reactionString = it } - reactionCount = getInt(R.styleable.ReactionButton_reaction_count, 0) - val status = getBoolean(R.styleable.ReactionButton_toggled, false) setChecked(status) } @@ -137,12 +92,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, setOnLongClickListener(this) } - private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? { - val id = array.getResourceId(styleableIndexId, -1) - - return if (-1 != id) ContextCompat.getDrawable(context, id) else null - } - /** * This triggers the entire functionality of the button such as icon changes, * animations, listeners etc. @@ -153,164 +102,25 @@ class ReactionButton @JvmOverloads constructor(context: Context, if (!isEnabled) { return } - isChecked = !isChecked - // icon!!.setImageDrawable(if (isChecked) likeDrawable else unLikeDrawable) background = if (isChecked) onDrawable else offDrawable if (isChecked) { reactedListener?.onReacted(this) - } else { - reactedListener?.onUnReacted(this) - } - - if (animatorSet != null) { - animatorSet!!.cancel() - } - - if (isChecked) { views.reactionText.animate().cancel() views.reactionText.scaleX = 0f views.reactionText.scaleY = 0f - - views.circle.innerCircleRadiusProgress = 0f - views.circle.outerCircleRadiusProgress = 0f - views.dots.currentProgress = 0f - - animatorSet = AnimatorSet() - - val outerCircleAnimator = ObjectAnimator.ofFloat(views.circle, CircleView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f) - outerCircleAnimator.duration = 250 - outerCircleAnimator.interpolator = DECCELERATE_INTERPOLATOR - - val innerCircleAnimator = ObjectAnimator.ofFloat(views.circle, CircleView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f) - innerCircleAnimator.duration = 200 - innerCircleAnimator.startDelay = 200 - innerCircleAnimator.interpolator = DECCELERATE_INTERPOLATOR - - val starScaleYAnimator = ObjectAnimator.ofFloat(views.reactionText, ImageView.SCALE_Y, 0.2f, 1f) - starScaleYAnimator.duration = 350 - starScaleYAnimator.startDelay = 250 - starScaleYAnimator.interpolator = OVERSHOOT_INTERPOLATOR - - val starScaleXAnimator = ObjectAnimator.ofFloat(views.reactionText, ImageView.SCALE_X, 0.2f, 1f) - starScaleXAnimator.duration = 350 - starScaleXAnimator.startDelay = 250 - starScaleXAnimator.interpolator = OVERSHOOT_INTERPOLATOR - - val dotsAnimator = ObjectAnimator.ofFloat(views.dots, DotsView.DOTS_PROGRESS, 0f, 1f) - // .ofFloat(views.dots, DotsView.DOTS_PROGRESS, 0, 1f) - dotsAnimator.duration = 900 - dotsAnimator.startDelay = 50 - dotsAnimator.interpolator = ACCELERATE_DECELERATE_INTERPOLATOR - - animatorSet!!.playTogether( - outerCircleAnimator, - innerCircleAnimator, - starScaleYAnimator, - starScaleXAnimator, - dotsAnimator - ) - - animatorSet!!.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationCancel(animation: Animator) { - views.circle.innerCircleRadiusProgress = 0f - views.circle.outerCircleRadiusProgress = 0f - views.dots.currentProgress = 0f - views.reactionText.scaleX = 1f - views.reactionText.scaleY = 1f - } - - override fun onAnimationEnd(animation: Animator) { -// if (animationEndListener != null) { -// // animationEndListener!!.onAnimationEnd(this@ReactionButton) -// } - } - }) - - animatorSet!!.start() + } else { + reactedListener?.onUnReacted(this) } } - /** - * Used to trigger the scale animation that takes places on the - * icon when the button is touched. - * - * @param event - * @return - */ -// override fun onTouchEvent(event: MotionEvent): Boolean { -// if (!isEnabled) -// return true -// -// when (event.action) { -// MotionEvent.ACTION_DOWN -> -// /* -// Commented out this line and moved the animation effect to the action up event due to -// conflicts that were occurring when library is used in sliding type views. -// -// icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR); -// */ -// isPressed = true -// -// MotionEvent.ACTION_MOVE -> { -// val x = event.x -// val y = event.y -// val isInside = x > 0 && x < width && y > 0 && y < height -// if (isPressed != isInside) { -// isPressed = isInside -// } -// } -// -// MotionEvent.ACTION_UP -> { -// views.reactionText!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR -// views.reactionText!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR -// if (isPressed) { -// performClick() -// isPressed = false -// } -// } -// MotionEvent.ACTION_CANCEL -> isPressed = false -// } -// return true -// } - override fun onLongClick(v: View?): Boolean { reactedListener?.onLongClick(this) return reactedListener != null } - /** - * This set sets the colours that are used for the little dots - * that will be exploding once the like button is clicked. - * - * @param primaryColor - * @param secondaryColor - */ - fun setExplodingDotColorsRes(@ColorRes primaryColor: Int, @ColorRes secondaryColor: Int) { - views.dots.setColors(ContextCompat.getColor(context, primaryColor), ContextCompat.getColor(context, secondaryColor)) - } - - fun setExplodingDotColorsInt(@ColorInt primaryColor: Int, @ColorInt secondaryColor: Int) { - views.dots.setColors(primaryColor, secondaryColor) - } - - fun setCircleStartColorRes(@ColorRes circleStartColor: Int) { - this.circleStartColor = ContextCompat.getColor(context, circleStartColor) - views.circle.startColor = this.circleStartColor - } - - fun setCircleStartColorInt(@ColorInt circleStartColor: Int) { - this.circleStartColor = circleStartColor - views.circle.startColor = circleStartColor - } - - fun setCircleEndColorRes(@ColorRes circleEndColor: Int) { - this.circleEndColor = ContextCompat.getColor(context, circleEndColor) - views.circle.endColor = this.circleEndColor - } - /** * Sets the initial state of the button to liked * or unliked. @@ -327,13 +137,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, } } - /** - * Sets the factor by which the dots should be sized. - */ - fun setAnimationScaleFactor(animationScaleFactor: Float) { - this.animationScaleFactor = animationScaleFactor - } - interface ReactedListener { fun onReacted(reactionButton: ReactionButton) fun onUnReacted(reactionButton: ReactionButton) 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 07dfadb753..4afda8a0e9 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 @@ -53,7 +53,10 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC addFragment( R.id.simpleFragmentContainer, CreateRoomFragment::class.java, - CreateRoomArgs(intent?.getStringExtra(INITIAL_NAME) ?: "") + CreateRoomArgs( + intent?.getStringExtra(INITIAL_NAME) ?: "", + isSpace = intent?.getBooleanExtra(IS_SPACE, false) ?: false + ) ) } } @@ -74,10 +77,12 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC companion object { private const val INITIAL_NAME = "INITIAL_NAME" + private const val IS_SPACE = "IS_SPACE" - fun getIntent(context: Context, initialName: String = ""): Intent { + fun getIntent(context: Context, initialName: String = "", isSpace: Boolean = false): Intent { return Intent(context, CreateRoomActivity::class.java).apply { putExtra(INITIAL_NAME, initialName) + putExtra(IS_SPACE, isSpace) } } } 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 fdc2bd8562..70f041bd69 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 @@ -38,6 +38,7 @@ import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentCreateRoomBinding +import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet @@ -51,11 +52,13 @@ import javax.inject.Inject @Parcelize data class CreateRoomArgs( val initialName: String, - val parentSpaceId: String? = null + val parentSpaceId: String? = null, + val isSpace: Boolean = false ) : Parcelable class CreateRoomFragment @Inject constructor( private val createRoomController: CreateRoomController, + private val createSpaceController: CreateSubSpaceController, val createRoomViewModelFactory: CreateRoomViewModel.Factory, colorProvider: ColorProvider ) : VectorBaseFragment(), @@ -93,6 +96,11 @@ class CreateRoomFragment @Inject constructor( } } + override fun onResume() { + super.onResume() + views.createRoomTitle.text = getString(if (args.isSpace) R.string.create_new_space else R.string.create_new_room) + } + private fun setupRoomJoinRuleSharedActionViewModel() { roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) roomJoinRuleSharedActionViewModel @@ -112,18 +120,26 @@ class CreateRoomFragment @Inject constructor( private fun setupWaitingView() { views.waitingView.waitingStatusText.isVisible = true - views.waitingView.waitingStatusText.setText(R.string.create_room_in_progress) + views.waitingView.waitingStatusText.setText( + if (args.isSpace) R.string.create_space_in_progress else R.string.create_room_in_progress + ) } override fun onDestroyView() { views.createRoomForm.cleanup() createRoomController.listener = null + createSpaceController.listener = null super.onDestroyView() } private fun setupRecyclerView() { - views.createRoomForm.configureWith(createRoomController) - createRoomController.listener = this + if (args.isSpace) { + views.createRoomForm.configureWith(createSpaceController) + createSpaceController.listener = this + } else { + views.createRoomForm.configureWith(createRoomController) + createRoomController.listener = this + } } override fun onAvatarDelete() { @@ -153,7 +169,11 @@ class CreateRoomFragment @Inject constructor( } else { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) } - RoomJoinRuleBottomSheet.newInstance(state.roomJoinRules, allowed.map { it.toOption(false) }) + RoomJoinRuleBottomSheet.newInstance(state.roomJoinRules, + allowed.map { it.toOption(false) }, + state.isSubSpace, + state.parentSpaceSummary?.displayName + ) .show(childFragmentManager, "RoomJoinRuleBottomSheet") } // override fun setIsPublic(isPublic: Boolean) { @@ -203,12 +223,24 @@ class CreateRoomFragment @Inject constructor( views.waitingView.root.isVisible = async is Loading if (async is Success) { // Navigate to freshly created room - navigator.openRoom(requireActivity(), async()) + if (state.isSubSpace) { + navigator.switchToSpace( + requireContext(), + async(), + Navigator.PostSwitchSpaceAction.None + ) + } else { + navigator.openRoom(requireActivity(), async()) + } sharedActionViewModel.post(RoomDirectorySharedAction.Close) } else { // Populate list with Epoxy - createRoomController.setData(state) + if (args.isSpace) { + createSpaceController.setData(state) + } else { + createRoomController.setData(state) + } } } } 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 fb70003dcf..9a9812933b 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 @@ -42,9 +42,11 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset @@ -241,6 +243,18 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init name = state.roomName.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() } avatarUri = state.avatarUri + + if (state.isSubSpace) { + // Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space + roomType = RoomType.SPACE + + // Space-rooms should be created with a power level for events_default of 100, + // to prevent the rooms accidentally/maliciously clogging up with messages from random members of the space. + powerLevelContentOverride = PowerLevelsContent( + eventsDefault = 100 + ) + } + when (state.roomJoinRules) { RoomJoinRules.PUBLIC -> { // Directory visibility 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 06742ea690..db56a19904 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 @@ -37,12 +37,14 @@ data class CreateRoomViewState( val parentSpaceId: String?, val parentSpaceSummary: RoomSummary? = null, val supportsRestricted: Boolean = false, - val aliasLocalPart: String? = null + val aliasLocalPart: String? = null, + val isSubSpace: Boolean = false ) : MvRxState { constructor(args: CreateRoomArgs) : this( roomName = args.initialName, - parentSpaceId = args.parentSpaceId + parentSpaceId = args.parentSpaceId, + isSubSpace = args.isSpace ) /** 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 new file mode 100644 index 0000000000..6d292c85da --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt @@ -0,0 +1,153 @@ +/* + * 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.roomdirectory.createroom + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import im.vector.app.R +import im.vector.app.core.epoxy.profiles.buildProfileAction +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericPillItem +import im.vector.app.features.discovery.settingsSectionTitleItem +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.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +class CreateSubSpaceController @Inject constructor( + private val stringProvider: StringProvider, + private val roomAliasErrorFormatter: RoomAliasErrorFormatter +) : TypedEpoxyController() { + + var listener: CreateRoomController.Listener? = null + + override fun buildModels(viewState: CreateRoomViewState) { + // display the form + buildForm(viewState, viewState.asyncCreateRoomRequest !is Loading) + } + + private fun buildForm(data: CreateRoomViewState, enableFormElement: Boolean) { + val host = this + + genericPillItem { + id("beta") + imageRes(R.drawable.ic_beta_pill) + tintIcon(false) + text(host.stringProvider.getString(R.string.space_add_space_to_any_space_you_manage)) + } + + formEditableSquareAvatarItem { + id("avatar") + enabled(enableFormElement) + imageUri(data.avatarUri) + clickListener { host.listener?.onAvatarChange() } + deleteListener { host.listener?.onAvatarDelete() } + } + + formEditTextItem { + id("name") + enabled(enableFormElement) + enabled(true) + value(data.roomName) + hint(host.stringProvider.getString(R.string.create_room_name_hint)) + onTextChange { text -> + host.listener?.onNameChange(text) + } + } + + if (data.roomJoinRules == RoomJoinRules.PUBLIC) { + formEditTextItem { + id("alias") + enabled(enableFormElement) + value(data.aliasLocalPart) + hint(host.stringProvider.getString(R.string.create_space_alias_hint)) + suffixText(":" + data.homeServerName) + prefixText("#") + errorMessage( + host.roomAliasErrorFormatter.format( + (((data.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) + ) + onTextChange { value -> + host.listener?.setAliasLocalPart(value) + } + } + } + + formMultiLineEditTextItem { + id("topic") + enabled(enableFormElement) + value(data.roomTopic) + hint(host.stringProvider.getString(R.string.create_space_topic_hint)) + textSizeSp(16) + onTextChange { text -> + host.listener?.onTopicChange(text) + } + } + + settingsSectionTitleItem { + id("visibility") + titleResId(R.string.room_settings_space_access_title) + } + + when (data.roomJoinRules) { + RoomJoinRules.INVITE -> { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_private_title), + subtitle = stringProvider.getString(R.string.room_settings_room_access_private_description), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + RoomJoinRules.PUBLIC -> { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_public_title), + subtitle = stringProvider.getString(R.string.room_settings_room_access_public_description), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + RoomJoinRules.RESTRICTED -> { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_restricted_title), + subtitle = stringProvider.getString(R.string.room_create_member_of_space_name_can_join, data.parentSpaceSummary?.displayName), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + else -> { + // not yet supported + } + } + + formSubmitButtonItem { + id("submit") + enabled(enableFormElement && data.roomName.isNullOrBlank().not()) + buttonTitleId(R.string.create_room_action_create) + buttonClickListener { host.listener?.submit() } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt index db1ba95e9e..e898bb2230 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt @@ -60,9 +60,11 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() // Avatar GlideApp.with(holder.avatarView) .load(directoryAvatarUrl) - .apply { + .let { if (!includeAllNetworks) { - placeholder(R.drawable.network_matrix) + it.placeholder(R.drawable.network_matrix) + } else { + it } } .into(holder.avatarView) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 9ea9f69383..08eab292a8 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -45,8 +45,8 @@ class RoomMemberProfileController @Inject constructor( fun onJumpToReadReceiptClicked() fun onMentionClicked() fun onEditPowerLevel(currentRole: Role) - fun onKickClicked() - fun onBanClicked(isUserBanned: Boolean) + fun onKickClicked(isSpace: Boolean) + fun onBanClicked(isSpace: Boolean, isUserBanned: Boolean) fun onCancelInviteClicked() fun onInviteClicked() } @@ -85,7 +85,9 @@ class RoomMemberProfileController @Inject constructor( } private fun buildRoomMemberActions(state: RoomMemberProfileViewState) { - buildSecuritySection(state) + if (!state.isSpace) { + buildSecuritySection(state) + } buildMoreSection(state) buildAdminSection(state) } @@ -181,7 +183,7 @@ class RoomMemberProfileController @Inject constructor( action = { callback?.onOpenDmClicked() } ) - if (state.hasReadReceipt) { + if (!state.isSpace && state.hasReadReceipt) { buildProfileAction( id = "read_receipt", editable = false, @@ -191,16 +193,18 @@ class RoomMemberProfileController @Inject constructor( } val ignoreActionTitle = state.buildIgnoreActionTitle() - - buildProfileAction( - id = "mention", - title = stringProvider.getString(R.string.room_participants_action_mention), - editable = false, - divider = ignoreActionTitle != null, - action = { callback?.onMentionClicked() } - ) + if (!state.isSpace) { + buildProfileAction( + id = "mention", + title = stringProvider.getString(R.string.room_participants_action_mention), + editable = false, + divider = ignoreActionTitle != null, + action = { callback?.onMentionClicked() } + ) + } val canInvite = state.actionPermissions.canInvite + if (canInvite && (membership == Membership.LEAVE || membership == Membership.KNOCK)) { buildProfileAction( id = "invite", @@ -261,7 +265,7 @@ class RoomMemberProfileController @Inject constructor( divider = canBan, destructive = true, title = stringProvider.getString(R.string.room_participants_action_kick), - action = { callback?.onKickClicked() } + action = { callback?.onKickClicked(state.isSpace) } ) } Membership.INVITE -> { @@ -288,7 +292,7 @@ class RoomMemberProfileController @Inject constructor( editable = false, destructive = true, title = banActionTitle, - action = { callback?.onBanClicked(membership == Membership.BAN) } + action = { callback?.onBanClicked(state.isSpace, membership == Membership.BAN) } ) } } 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 0adc1531c9..d8b4f249da 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 @@ -305,16 +305,16 @@ class RoomMemberProfileFragment @Inject constructor( val views = DialogShareQrCodeBinding.bind(view) views.itemShareQrCodeImage.setData(permalink) MaterialAlertDialogBuilder(requireContext()) - .setView(view) - .setNeutralButton(R.string.ok, null) - .setPositiveButton(R.string.share_by_text) { _, _ -> - startSharePlainTextIntent( - fragment = this, - activityResultLauncher = null, - chooserTitle = null, - text = permalink - ) - }.show() + .setView(view) + .setNeutralButton(R.string.ok, null) + .setPositiveButton(R.string.share_by_text) { _, _ -> + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = null, + text = permalink + ) + }.show() } private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) { @@ -327,12 +327,13 @@ class RoomMemberProfileFragment @Inject constructor( } } - override fun onKickClicked() { + override fun onKickClicked(isSpace: Boolean) { ConfirmationDialogBuilder .show( activity = requireActivity(), askForReason = true, - confirmationRes = R.string.room_participants_kick_prompt_msg, + confirmationRes = if (isSpace) R.string.space_participants_kick_prompt_msg + else R.string.room_participants_kick_prompt_msg, positiveRes = R.string.room_participants_action_kick, reasonHintRes = R.string.room_participants_kick_reason, titleRes = R.string.room_participants_kick_title @@ -341,16 +342,18 @@ class RoomMemberProfileFragment @Inject constructor( } } - override fun onBanClicked(isUserBanned: Boolean) { + override fun onBanClicked(isSpace: Boolean, isUserBanned: Boolean) { val titleRes: Int val positiveButtonRes: Int val confirmationRes: Int if (isUserBanned) { - confirmationRes = R.string.room_participants_unban_prompt_msg + confirmationRes = if (isSpace) R.string.space_participants_unban_prompt_msg + else R.string.room_participants_unban_prompt_msg titleRes = R.string.room_participants_unban_title positiveButtonRes = R.string.room_participants_action_unban } else { - confirmationRes = R.string.room_participants_ban_prompt_msg + confirmationRes = if (isSpace) R.string.space_participants_ban_prompt_msg + else R.string.room_participants_ban_prompt_msg titleRes = R.string.room_participants_ban_title positiveButtonRes = R.string.room_participants_action_ban } 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 6978a24747..4b57bdc6aa 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 @@ -48,6 +48,7 @@ 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 @@ -86,7 +87,8 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v copy( isMine = session.myUserId == this.userId, userMatrixItem = room?.getRoomMember(initialState.userId)?.toMatrixItem()?.let { Success(it) } ?: Uninitialized, - hasReadReceipt = room?.getUserReadReceipt(initialState.userId) != null + hasReadReceipt = room?.getUserReadReceipt(initialState.userId) != null, + isSpace = room?.roomSummary()?.roomType == RoomType.SPACE ) } observeIgnoredState() 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 f943a5cf08..5c2751f0dc 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 @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.util.MatrixItem data class RoomMemberProfileViewState( val userId: String, val roomId: String?, + val isSpace: Boolean = false, val showAsMember: Boolean = false, val isMine: Boolean = false, val isIgnored: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt index bb1054b704..b083209f16 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt @@ -23,9 +23,12 @@ import org.matrix.android.sdk.api.session.events.model.EventType /** * Change on each permission has an effect on the power level event. Try to sort the effect by category. */ -sealed class EditablePermission(@StringRes val labelResId: Int) { +sealed class EditablePermission(@StringRes val labelResId: Int, @StringRes val spaceLabelResId: Int = labelResId) { // Updates `content.events.[eventType]` - open class EventTypeEditablePermission(val eventType: String, @StringRes labelResId: Int) : EditablePermission(labelResId) + open class EventTypeEditablePermission(val eventType: String, + @StringRes labelResId: Int, + @StringRes spaceLabelResId: Int = labelResId + ) : EditablePermission(labelResId, spaceLabelResId) class ModifyWidgets : EventTypeEditablePermission( // Note: Element Web still use legacy value @@ -35,17 +38,20 @@ sealed class EditablePermission(@StringRes val labelResId: Int) { class ChangeRoomAvatar : EventTypeEditablePermission( EventType.STATE_ROOM_AVATAR, - R.string.room_permissions_change_room_avatar + R.string.room_permissions_change_room_avatar, + R.string.room_permissions_change_space_avatar ) class ChangeMainAddressForTheRoom : EventTypeEditablePermission( EventType.STATE_ROOM_CANONICAL_ALIAS, - R.string.room_permissions_change_main_address_for_the_room + R.string.room_permissions_change_main_address_for_the_room, + R.string.room_permissions_change_main_address_for_the_space ) class EnableRoomEncryption : EventTypeEditablePermission( EventType.STATE_ROOM_ENCRYPTION, - R.string.room_permissions_enable_room_encryption + R.string.room_permissions_enable_room_encryption, + R.string.room_permissions_enable_space_encryption ) class ChangeHistoryVisibility : EventTypeEditablePermission( @@ -55,7 +61,8 @@ sealed class EditablePermission(@StringRes val labelResId: Int) { class ChangeRoomName : EventTypeEditablePermission( EventType.STATE_ROOM_NAME, - R.string.room_permissions_change_room_name + R.string.room_permissions_change_room_name, + R.string.room_permissions_change_space_name ) class ChangePermissions : EventTypeEditablePermission( @@ -70,7 +77,8 @@ sealed class EditablePermission(@StringRes val labelResId: Int) { class UpgradeTheRoom : EventTypeEditablePermission( EventType.STATE_ROOM_TOMBSTONE, - R.string.room_permissions_upgrade_the_room + R.string.room_permissions_upgrade_the_room, + R.string.room_permissions_upgrade_the_space ) class ChangeTopic : EventTypeEditablePermission( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt index ead878936e..1f2c876902 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt @@ -26,6 +26,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.form.formAdvancedToggleItem import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.banOrDefault import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault import org.matrix.android.sdk.api.session.room.model.inviteOrDefault @@ -57,6 +58,13 @@ class RoomPermissionsController @Inject constructor( EditablePermission.ChangeTopic() ) + private val usefulEditablePermissionsForSpace = listOf( + EditablePermission.ChangeRoomAvatar(), + EditablePermission.ChangeRoomName(), + EditablePermission.ChangeTopic(), + EditablePermission.InviteUsers() + ) + private val advancedEditablePermissions = listOf( EditablePermission.ChangeMainAddressForTheRoom(), @@ -79,6 +87,27 @@ class RoomPermissionsController @Inject constructor( EditablePermission.UpgradeTheRoom() ) + private val advancedEditablePermissionsForSpace = listOf( + EditablePermission.ChangeMainAddressForTheRoom(), + + EditablePermission.DefaultRole(), + EditablePermission.KickUsers(), + EditablePermission.BanUsers(), + + EditablePermission.SendMessages(), + + EditablePermission.RemoveMessagesSentByOthers(), + EditablePermission.NotifyEveryone(), + + EditablePermission.ChangeSettings(), +// EditablePermission.ModifyWidgets(), + EditablePermission.ChangeHistoryVisibility(), + EditablePermission.ChangePermissions(), + EditablePermission.SendRoomServerAclEvents(), +// EditablePermission.EnableRoomEncryption(), + EditablePermission.UpgradeTheRoom() + ) + init { setData(null) } @@ -103,13 +132,24 @@ class RoomPermissionsController @Inject constructor( private fun buildPermissions(data: RoomPermissionsViewState, content: PowerLevelsContent) { val host = this val editable = data.actionPermissions.canChangePowerLevels + val isSpace = data.roomSummary.invoke()?.roomType == RoomType.SPACE + settingsInfoItem { id("notice") - helperText(host.stringProvider.getString(if (editable) R.string.room_permissions_notice else R.string.room_permissions_notice_read_only)) + helperText(host.stringProvider.getString( + if (editable) { + if (isSpace) R.string.space_permissions_notice else R.string.room_permissions_notice + } else { + if (isSpace) R.string.space_permissions_notice_read_only else R.string.room_permissions_notice_read_only + })) } // Useful permissions - usefulEditablePermissions.forEach { buildPermission(it, content, editable) } + if (isSpace) { + usefulEditablePermissionsForSpace.forEach { buildPermission(it, content, editable, true) } + } else { + usefulEditablePermissions.forEach { buildPermission(it, content, editable, false) } + } // Toggle formAdvancedToggleItem { @@ -121,15 +161,24 @@ class RoomPermissionsController @Inject constructor( // Advanced permissions if (data.showAdvancedPermissions) { - advancedEditablePermissions.forEach { buildPermission(it, content, editable) } + if (isSpace) { + advancedEditablePermissionsForSpace.forEach { buildPermission(it, content, editable, true) } + } else { + advancedEditablePermissions.forEach { buildPermission(it, content, editable, false) } + } } } - private fun buildPermission(editablePermission: EditablePermission, content: PowerLevelsContent, editable: Boolean) { + private fun buildPermission(editablePermission: EditablePermission, + content: PowerLevelsContent, + editable: Boolean, + isSpace: Boolean) { val currentRole = getCurrentRole(editablePermission, content) buildProfileAction( id = editablePermission.labelResId.toString(), - title = stringProvider.getString(editablePermission.labelResId), + title = stringProvider.getString( + if (isSpace) editablePermission.spaceLabelResId else editablePermission.labelResId + ), subtitle = roleFormatter.format(currentRole), divider = true, editable = editable, 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 1265e7e5ee..e872a04d80 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 @@ -312,6 +312,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: _viewEvents.post(RoomSettingsViewEvents.Failure(it)) } ) + .disposeOnClear() } private fun postLoading(isLoading: Boolean) { 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 f4c7eecf8f..f0f8193cc5 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 @@ -39,7 +39,9 @@ fun RoomJoinRules.toOption(needUpgrade: Boolean) = JoinRulesOptionSupport(this, @Parcelize data class RoomJoinRuleBottomSheetArgs( val currentRoomJoinRule: RoomJoinRules, - val allowedJoinedRules: List + val allowedJoinedRules: List, + val isSpace: Boolean = false, + val parentSpaceName: String? ) : Parcelable class RoomJoinRuleBottomSheet : BottomSheetGeneric() { @@ -73,11 +75,13 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric = listOf( RoomJoinRules.INVITE, RoomJoinRules.PUBLIC - ).map { it.toOption(true) } + ).map { it.toOption(true) }, + isSpace: Boolean = false, + parentSpaceName: String? = null ): RoomJoinRuleBottomSheet { return RoomJoinRuleBottomSheet().apply { setArguments( - RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, allowedJoinedRules) + RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, allowedJoinedRules, isSpace, parentSpaceName) ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt index edeb6e1099..2552a2568c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt @@ -20,8 +20,6 @@ import im.vector.app.R import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController -import me.gujun.android.span.image -import me.gujun.android.span.span import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject @@ -30,7 +28,11 @@ class RoomJoinRuleController @Inject constructor( private val drawableProvider: DrawableProvider ) : BottomSheetGenericController() { - override fun getTitle() = stringProvider.getString(R.string.room_settings_room_access_rules_pref_dialog_title) + override fun getTitle() = + stringProvider.getString( + // generic title for both room and space + R.string.room_settings_access_rules_pref_dialog_title + ) override fun getActions(state: RoomJoinRuleState): List { return listOf( @@ -42,21 +44,21 @@ class RoomJoinRuleController @Inject constructor( ), RoomJoinRuleRadioAction( roomJoinRule = RoomJoinRules.PUBLIC, - description = stringProvider.getString(R.string.room_settings_room_access_public_description), + description = stringProvider.getString( + if (state.isSpace) R.string.room_settings_space_access_public_description + else R.string.room_settings_room_access_public_description + ), title = stringProvider.getString(R.string.room_settings_room_access_public_title), isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC ), RoomJoinRuleRadioAction( roomJoinRule = RoomJoinRules.RESTRICTED, - description = stringProvider.getString(R.string.room_settings_room_access_restricted_description), - title = span { - +stringProvider.getString(R.string.room_settings_room_access_restricted_title) - +" " - image( - drawableProvider.getDrawable(R.drawable.ic_beta_pill)!!, - "bottom" - ) + description = if (state.parentSpaceName != null) { + stringProvider.getString(R.string.room_create_member_of_space_name_can_join, state.parentSpaceName) + } else { + stringProvider.getString(R.string.room_settings_room_access_restricted_description) }, + title = stringProvider.getString(R.string.room_settings_room_access_restricted_title), isSelected = state.currentRoomJoinRule == RoomJoinRules.RESTRICTED ) ).filter { state.allowedJoinedRules.map { it.rule }.contains(it.roomJoinRule) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt index dd818a4631..dcf115cc4b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt @@ -24,11 +24,15 @@ data class RoomJoinRuleState( val currentRoomJoinRule: RoomJoinRules = RoomJoinRules.INVITE, val allowedJoinedRules: List = listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC).map { it.toOption(true) }, - val currentGuestAccess: GuestAccess? = null + val currentGuestAccess: GuestAccess? = null, + val isSpace: Boolean = false, + val parentSpaceName: String? ) : BottomSheetGenericState() { constructor(args: RoomJoinRuleBottomSheetArgs) : this( currentRoomJoinRule = args.currentRoomJoinRule, - allowedJoinedRules = args.allowedJoinedRules + allowedJoinedRules = args.allowedJoinedRules, + isSpace = args.isSpace, + parentSpaceName = args.parentSpaceName ) } 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..4341d381b6 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 @@ -117,6 +117,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 +160,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 +314,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) } 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..caf42e7cf9 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 @@ -29,6 +29,7 @@ import im.vector.app.databinding.ActivityVectorSettingsBinding import im.vector.app.features.discovery.DiscoverySettingsFragment 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 @@ -136,6 +137,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") 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/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index 0d178bddea..4e1c62a4ec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -37,7 +37,6 @@ class LocalePickerController @Inject constructor( var listener: Listener? = null - @ExperimentalStdlibApi override fun buildModels(data: LocalePickerViewState?) { val list = data?.locales ?: return val host = this 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..098d1b2caa 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,14 @@ 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 javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -116,11 +123,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 } @@ -343,3 +390,43 @@ 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>> { + return getThreePidsLive(refreshData = false) + .distinctUntilChanged() + .map { threePids -> + val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL } + threePids + .filterIsInstance() + .map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } } + } +} 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/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/InitialSyncProgressService.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.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/settings/push/PushGatewayViewEvents.kt index b5d4ef4dbb..8b2a833b5c 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/settings/push/PushGatewayViewEvents.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,11 @@ * 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.settings.push -interface InitialSyncProgressService { +import im.vector.app.core.platform.VectorViewEvents - fun getInitialSyncProgressStatus(): LiveData - - sealed class Status { - object Idle : Status() - data class Progressing( - val initSyncStep: InitSyncStep, - val percentProgress: Int = 0 - ) : Status() - } +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..cd1899741f 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,11 +24,14 @@ 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 @@ -64,7 +67,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..9a47fa2a15 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 @@ -16,6 +16,7 @@ package im.vector.app.features.settings.push +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState @@ -26,8 +27,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory 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 @@ -38,7 +39,7 @@ data class PushGatewayViewState( class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedFactory interface Factory { @@ -70,10 +71,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/spaces/LeaveSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt new file mode 100644 index 0000000000..792c1e2081 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt @@ -0,0 +1,196 @@ +/* + * 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.spaces + +import android.app.Activity +import android.graphics.Typeface +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.toSpannable +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.args +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.checkedChanges +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.registerStartForActivityResult +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.styleMatchingText +import im.vector.app.databinding.BottomSheetLeaveSpaceBinding +import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.parcelize.Parcelize +import me.gujun.android.span.span +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { + + val settingsViewModel: SpaceMenuViewModel by parentFragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetLeaveSpaceBinding { + return BottomSheetLeaveSpaceBinding.inflate(inflater, container, false) + } + + @Inject lateinit var colorProvider: ColorProvider + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + @Parcelize + data class Args( + val spaceId: String + ) : Parcelable + + override val showExpanded = true + + private val spaceArgs: SpaceBottomSheetSettingsArgs by args() + + private val cherryPickLeaveActivityResult = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + // nothing actually? + } else { + // move back to default + settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.autoLeaveRadioGroup.checkedChanges() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + when (it) { + views.leaveAll.id -> { + settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll) + } + views.leaveNone.id -> { + settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveNone) + } + views.leaveSelected.id -> { + settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveSelected) + // launch dedicated activity + cherryPickLeaveActivityResult.launch( + SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId) + ) + } + } + } + .disposeOnDestroyView() + + views.leaveButton.debouncedClicks { + settingsViewModel.handle(SpaceLeaveViewAction.LeaveSpace) + } + + views.cancelButton.debouncedClicks { + dismiss() + } + } + + override fun invalidate() = withState(settingsViewModel) { state -> + super.invalidate() + + val spaceSummary = state.spaceSummary ?: return@withState + val bestName = spaceSummary.toMatrixItem().getBestName() + val commonText = getString(R.string.space_leave_prompt_msg_with_name, bestName) + .toSpannable().styleMatchingText(bestName, Typeface.BOLD) + + val warningMessage: CharSequence = if (spaceSummary.otherMemberIds.isEmpty()) { + span { + +commonText + +"\n\n" + span(getString(R.string.space_leave_prompt_msg_only_you)) { + textColor = colorProvider.getColorFromAttribute(R.attr.colorError) + } + } + } else if (state.isLastAdmin) { + span { + +commonText + +"\n\n" + span(getString(R.string.space_leave_prompt_msg_as_admin)) { + textColor = colorProvider.getColorFromAttribute(R.attr.colorError) + } + } + } else if (!spaceSummary.isPublic) { + span { + +commonText + +"\n\n" + span(getString(R.string.space_leave_prompt_msg_private)) { + textColor = colorProvider.getColorFromAttribute(R.attr.colorError) + } + } + } else { + commonText + } + + views.bottomLeaveSpaceWarningText.setTextOrHide(warningMessage) + + views.inlineErrorText.setTextOrHide(null) + if (state.leavingState is Loading) { + views.leaveButton.isInvisible = true + views.cancelButton.isInvisible = true + views.leaveProgress.isVisible = true + } else { + views.leaveButton.isInvisible = false + views.cancelButton.isInvisible = false + views.leaveProgress.isVisible = false + if (state.leavingState is Fail) { + views.inlineErrorText.setTextOrHide(errorFormatter.toHumanReadable(state.leavingState.error)) + } + } + + val hasChildren = (spaceSummary.spaceChildren?.size ?: 0) > 0 + if (hasChildren) { + views.autoLeaveRadioGroup.isVisible = true + when (state.leaveMode) { + SpaceMenuState.LeaveMode.LEAVE_ALL -> { + views.autoLeaveRadioGroup.check(views.leaveAll.id) + } + SpaceMenuState.LeaveMode.LEAVE_NONE -> { + views.autoLeaveRadioGroup.check(views.leaveNone.id) + } + SpaceMenuState.LeaveMode.LEAVE_SELECTED -> { + views.autoLeaveRadioGroup.check(views.leaveSelected.id) + } + } + } else { + views.autoLeaveRadioGroup.isVisible = false + } + } + + companion object { + + fun newInstance(spaceId: String) + : LeaveSpaceBottomSheet { + return LeaveSpaceBottomSheet().apply { + setArguments(SpaceBottomSheetSettingsArgs(spaceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index a02755a155..344559bc81 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -31,6 +31,7 @@ import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.CreateSpaceAction +import im.vector.app.features.spaces.create.CreateSpaceAdd3pidInvitesFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.create.CreateSpaceEvents @@ -55,18 +56,21 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac super.onCreate(savedInstanceState) if (isFirstCreation()) { when (withState(viewModel) { it.step }) { - CreateSpaceState.Step.ChooseType -> { + CreateSpaceState.Step.ChooseType -> { navigateToFragment(ChooseSpaceTypeFragment::class.java) } - CreateSpaceState.Step.SetDetails -> { + CreateSpaceState.Step.SetDetails -> { navigateToFragment(ChooseSpaceTypeFragment::class.java) } - CreateSpaceState.Step.AddRooms -> { + CreateSpaceState.Step.AddRooms -> { navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) } - CreateSpaceState.Step.ChoosePrivateType -> { + CreateSpaceState.Step.ChoosePrivateType -> { navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java) } + CreateSpaceState.Step.AddEmailsOrInvites -> { + navigateToFragment(CreateSpaceAdd3pidInvitesFragment::class.java) + } } } } @@ -92,6 +96,9 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac CreateSpaceEvents.NavigateToAddRooms -> { navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) } + CreateSpaceEvents.NavigateToAdd3Pid -> { + navigateToFragment(CreateSpaceAdd3pidInvitesFragment::class.java) + } CreateSpaceEvents.NavigateToChoosePrivateType -> { navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java) } @@ -143,6 +150,7 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac if (state.spaceType == SpaceType.Public) R.string.your_public_space else R.string.your_private_space } + CreateSpaceState.Step.AddEmailsOrInvites, CreateSpaceState.Step.ChoosePrivateType -> R.string.your_private_space } supportActionBar?.let { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceLeaveViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceLeaveViewAction.kt new file mode 100644 index 0000000000..0d17dddca9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceLeaveViewAction.kt @@ -0,0 +1,26 @@ +/* + * 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.spaces + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class SpaceLeaveViewAction : VectorViewModelAction { + object SetAutoLeaveAll : SpaceLeaveViewAction() + object SetAutoLeaveNone : SpaceLeaveViewAction() + object SetAutoLeaveSelected : SpaceLeaveViewAction() + object LeaveSpace : SpaceLeaveViewAction() +} 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 new file mode 100644 index 0000000000..395fcc9df1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt @@ -0,0 +1,39 @@ +/* + * 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.spaces + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class SpaceMenuState( + val spaceId: String, + val spaceSummary: RoomSummary? = null, + val canEditSettings: Boolean = false, + val canInvite: Boolean = false, + val canAddChild: Boolean = false, + val isLastAdmin: Boolean = false, + val leaveMode: LeaveMode = LeaveMode.LEAVE_NONE, + val leavingState: Async = Uninitialized +) : MvRxState { + constructor(args: SpaceBottomSheetSettingsArgs) : this(spaceId = args.spaceId) + + enum class LeaveMode { + LEAVE_ALL, LEAVE_NONE, LEAVE_SELECTED + } +} 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 new file mode 100644 index 0000000000..24ca218942 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt @@ -0,0 +1,171 @@ +/* + * 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.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.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.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.query.ActiveSpaceFilter +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.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 timber.log.Timber + +class SpaceMenuViewModel @AssistedInject constructor( + @Assisted val initialState: SpaceMenuState, + val session: Session, + val appStateHandler: AppStateHandler +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + 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") + } + } + + init { + val roomSummary = session.getRoomSummary(initialState.spaceId) + + setState { + copy(spaceSummary = roomSummary) + } + + session.getRoom(initialState.spaceId)?.let { room -> + + room.rx().liveRoomSummary().subscribe { + it.getOrNull()?.let { + if (it.membership == Membership.LEAVE) { + setState { copy(leavingState = Success(Unit)) } + if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) { + // switch to home? + appStateHandler.setCurrentSpace(null, session) + } + } + } + }.disposeOnClear() + + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + + val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) + val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) + + val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) + val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) + val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) + + val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin + val otherAdminCount = roomSummary?.otherMemberIds + ?.map { powerLevelsHelper.getUserRole(it) } + ?.count { it is Role.Admin } + ?: 0 + val isLastAdmin = isAdmin && otherAdminCount == 0 + + setState { + copy( + canEditSettings = canChangeAvatar || canChangeName || canChangeTopic, + canInvite = canInvite, + canAddChild = canAddChild, + isLastAdmin = isLastAdmin + ) + } + } + .disposeOnClear() + } + } + + override fun handle(action: SpaceLeaveViewAction) { + when (action) { + SpaceLeaveViewAction.SetAutoLeaveAll -> setState { + copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_ALL, leavingState = Uninitialized) + } + SpaceLeaveViewAction.SetAutoLeaveNone -> setState { + copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_NONE, leavingState = Uninitialized) + } + SpaceLeaveViewAction.SetAutoLeaveSelected -> setState { + copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_SELECTED, leavingState = Uninitialized) + } + SpaceLeaveViewAction.LeaveSpace -> handleLeaveSpace() + } + } + + private fun handleLeaveSpace() = withState { state -> + + setState { copy(leavingState = Loading()) } + + session.coroutineScope.launch { + try { + if (state.leaveMode == SpaceMenuState.LeaveMode.LEAVE_NONE) { + session.getRoom(initialState.spaceId)?.leave(null) + } else if (state.leaveMode == SpaceMenuState.LeaveMode.LEAVE_ALL) { + // need to find all child rooms that i have joined + + session.getRoomSummaries( + roomSummaryQueryParams { + excludeType = null + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(initialState.spaceId) + memberships = listOf(Membership.JOIN) + } + ).forEach { + try { + session.getRoom(it.roomId)?.leave(null) + } catch (failure: Throwable) { + // silently ignore? + Timber.e(failure, "Fail to leave sub rooms/spaces") + } + } + session.getRoom(initialState.spaceId)?.leave(null) + } + + // We observe the membership and to dismiss when we have remote echo of leaving + } catch (failure: Throwable) { + setState { copy(leavingState = Fail(failure)) } + } + } + } +} 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 37c5088123..040f1f9057 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 @@ -22,35 +22,23 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import com.airbnb.mvrx.Success import com.airbnb.mvrx.args -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState 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.resources.ColorProvider import im.vector.app.databinding.BottomSheetSpaceSettingsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.rageshake.BugReporter -import im.vector.app.features.rageshake.ReportType import im.vector.app.features.roomprofile.RoomProfileActivity -import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity -import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -import me.gujun.android.span.span import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.toMatrixItem -import timber.log.Timber import javax.inject.Inject @Parcelize @@ -58,15 +46,12 @@ data class SpaceBottomSheetSettingsArgs( val spaceId: String ) : Parcelable -// XXX make proper view model before leaving beta -class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { +class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceMenuViewModel.Factory { @Inject lateinit var navigator: Navigator - @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var avatarRenderer: AvatarRenderer - @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var colorProvider: ColorProvider + @Inject lateinit var viewModelFactory: SpaceMenuViewModel.Factory private val spaceArgs: SpaceBottomSheetSettingsArgs by args() @@ -74,6 +59,8 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) - val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) - val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) - - val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) - val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) - val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) - - views.spaceSettings.isVisible = canChangeAvatar || canChangeName || canChangeTopic - - views.invitePeople.isVisible = canInvite || roomSummary?.isPublic.orFalse() - views.addRooms.isVisible = canAddChild - - val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin - val otherAdminCount = roomSummary?.otherMemberIds - ?.map { powerLevelsHelper.getUserRole(it) } - ?.count { it is Role.Admin } - ?: 0 - isLastAdmin = isAdmin && otherAdminCount == 0 - }.disposeOnDestroyView() - - views.spaceBetaTag.debouncedClicks { - bugReporter.openBugReportScreen(requireActivity(), ReportType.SPACE_BETA_FEEDBACK) - } - views.invitePeople.views.bottomSheetActionClickableZone.debouncedClicks { dismiss() interactionListener?.onShareSpaceSelected(spaceArgs.spaceId) @@ -153,41 +101,34 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - session.coroutineScope.launch { - try { - session.getRoom(spaceArgs.spaceId)?.leave(null) - } catch (failure: Throwable) { - Timber.e(failure, "Failed to leave space") - } - } - dismiss() - } - .setNegativeButton(R.string.cancel, null) - .show() + views.addSpaces.views.bottomSheetActionClickableZone.debouncedClicks { + dismiss() + startActivity(SpaceManageActivity.newIntent(requireActivity(), spaceArgs.spaceId, ManageType.AddRoomsOnlySpaces)) } + + views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks { + LeaveSpaceBottomSheet.newInstance(spaceArgs.spaceId).show(childFragmentManager, "LOGOUT") + } + } + + override fun invalidate() = withState(settingsViewModel) { state -> + super.invalidate() + + if (state.leavingState is Success) { + dismiss() + } + + state.spaceSummary?.toMatrixItem()?.let { + avatarRenderer.render(it, views.spaceAvatarImageView) + } + views.spaceNameView.text = state.spaceSummary?.displayName + views.spaceDescription.setTextOrHide(state.spaceSummary?.topic?.takeIf { it.isNotEmpty() }) + + views.spaceSettings.isVisible = state.canEditSettings + + views.invitePeople.isVisible = state.canInvite || state.spaceSummary?.isPublic.orFalse() + views.addRooms.isVisible = state.canAddChild + views.addSpaces.isVisible = state.canAddChild } companion object { @@ -198,4 +139,8 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = - BottomSheetSpaceCreatePrivateWarningBinding.inflate(inflater, container, false) - - override val showExpanded = true - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - views.continueButton.debouncedClicks { - setFragmentResult(REQUEST_KEY, Bundle.EMPTY) - dismiss() - } - } - - companion object { - const val REQUEST_KEY = "BetaWarningBottomSheet" - } -} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt index 4031b56c1d..4f079551eb 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.setFragmentResultListener import com.airbnb.mvrx.activityViewModel import im.vector.app.R import im.vector.app.core.epoxy.onClick @@ -39,13 +38,6 @@ class ChoosePrivateSpaceTypeFragment @Inject constructor( override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceCreateChoosePrivateModelBinding.inflate(layoutInflater, container, false) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFragmentResultListener(BetaWarningBottomSheet.REQUEST_KEY) { _, _ -> - sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.MeAndTeammates)) - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -54,7 +46,7 @@ class ChoosePrivateSpaceTypeFragment @Inject constructor( } views.teammatesButton.onClick { - BetaWarningBottomSheet().show(parentFragmentManager, "warning") + sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.MeAndTeammates)) } sharedViewModel.subscribe { state -> diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt index 1f0ed6428f..e2eaa5784f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt @@ -28,6 +28,8 @@ sealed class CreateSpaceAction : VectorViewModelAction { object OnBackPressed : CreateSpaceAction() object NextFromDetails : CreateSpaceAction() object NextFromDefaultRooms : CreateSpaceAction() + object NextFromAdd3pid : CreateSpaceAction() data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction() + data class DefaultInvite3pidChanged(val index: Int, val email: String) : CreateSpaceAction() data class SetSpaceTopology(val topology: SpaceTopology) : CreateSpaceAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt new file mode 100644 index 0000000000..6dc3ad8c21 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAdd3pidInvitesFragment.kt @@ -0,0 +1,93 @@ +/* + * 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.spaces.create + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +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.OnBackPressed +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding +import im.vector.app.features.settings.VectorSettingsActivity +import javax.inject.Inject + +class CreateSpaceAdd3pidInvitesFragment @Inject constructor( + private val epoxyController: SpaceAdd3pidEpoxyController +) : VectorBaseFragment(), + SpaceAdd3pidEpoxyController.Listener, + OnBackPressed { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + sharedViewModel.handle(CreateSpaceAction.OnBackPressed) + return true + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.recyclerView.configureWith(epoxyController) + epoxyController.listener = this + + sharedViewModel.subscribe(this) { + invalidateState(it) + } + + views.nextButton.setText(R.string.next_pf) + views.nextButton.debouncedClicks { + view.hideKeyboard() + sharedViewModel.handle(CreateSpaceAction.NextFromAdd3pid) + } + } + + private fun invalidateState(it: CreateSpaceState) { + epoxyController.setData(it) + val noEmails = it.default3pidInvite?.all { it.value.isNullOrBlank() } ?: true + views.nextButton.text = if (noEmails) { + getString(R.string.skip_for_now) + } else { + getString(R.string.next_pf) + } + } + + override fun onDestroyView() { + views.recyclerView.cleanup() + epoxyController.listener = null + super.onDestroyView() + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + + override fun on3pidChange(index: Int, newName: String) { + sharedViewModel.handle(CreateSpaceAction.DefaultInvite3pidChanged(index, newName)) + } + + override fun onNoIdentityServer() { + navigator.openSettings( + requireContext(), + VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt index 073531353f..eeb2ca30ff 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt @@ -22,6 +22,7 @@ sealed class CreateSpaceEvents : VectorViewEvents { object NavigateToDetails : CreateSpaceEvents() object NavigateToChooseType : CreateSpaceEvents() object NavigateToAddRooms : CreateSpaceEvents() + object NavigateToAdd3Pid : CreateSpaceEvents() object NavigateToChoosePrivateType : CreateSpaceEvents() object Dismiss : CreateSpaceEvents() data class FinishSuccess(val spaceId: String, val defaultRoomId: String?, val topology: SpaceTopology?) : CreateSpaceEvents() 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 39a69e837b..6fb5853269 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 @@ -34,13 +34,17 @@ data class CreateSpaceState( val aliasVerificationTask: Async = Uninitialized, val nameInlineError: String? = null, val defaultRooms: Map? = null, - val creationResult: Async = Uninitialized + val default3pidInvite: Map? = null, + val emailValidationResult: Map? = null, + val creationResult: Async = Uninitialized, + val canInviteByMail: Boolean = false ) : MvRxState { enum class Step { ChooseType, SetDetails, AddRooms, - ChoosePrivateType + ChoosePrivateType, + AddEmailsOrInvites } } 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 2537a3a592..e6ead2294e 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 @@ -31,6 +31,7 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.isEmail import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers @@ -38,6 +39,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure @@ -49,12 +51,28 @@ class CreateSpaceViewModel @AssistedInject constructor( private val errorFormatter: ErrorFormatter ) : VectorViewModel(initialState) { + private val identityService = session.identityService() + + private val identityServerManagerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() { + val identityServerUrl = identityService.getCurrentIdentityServerUrl() + setState { + copy( + canInviteByMail = identityServerUrl != null + ) + } + } + } + init { + val identityServerUrl = identityService.getCurrentIdentityServerUrl() setState { copy( - homeServerName = session.myUserId.getDomain() + homeServerName = session.myUserId.getDomain(), + canInviteByMail = identityServerUrl != null ) } + startListenToIdentityManager() } @AssistedFactory @@ -62,6 +80,19 @@ class CreateSpaceViewModel @AssistedInject constructor( fun create(initialState: CreateSpaceState): CreateSpaceViewModel } + private fun startListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun stopListenToIdentityManager() { + identityService.removeListener(identityServerManagerListener) + } + + override fun onCleared() { + stopListenToIdentityManager() + super.onCleared() + } + companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? { @@ -84,7 +115,7 @@ class CreateSpaceViewModel @AssistedInject constructor( override fun handle(action: CreateSpaceAction) { when (action) { - is CreateSpaceAction.SetRoomType -> { + is CreateSpaceAction.SetRoomType -> { setState { copy( step = CreateSpaceState.Step.SetDetails, @@ -93,7 +124,7 @@ class CreateSpaceViewModel @AssistedInject constructor( } _viewEvents.post(CreateSpaceEvents.NavigateToDetails) } - is CreateSpaceAction.NameChanged -> { + is CreateSpaceAction.NameChanged -> { setState { if (aliasManuallyModified) { copy( @@ -113,14 +144,14 @@ class CreateSpaceViewModel @AssistedInject constructor( } } } - is CreateSpaceAction.TopicChanged -> { + is CreateSpaceAction.TopicChanged -> { setState { copy( topic = action.topic ) } } - is CreateSpaceAction.SpaceAliasChanged -> { + is CreateSpaceAction.SpaceAliasChanged -> { // This called only when the alias is change manually // not when programmatically changed via a change on name setState { @@ -131,28 +162,43 @@ class CreateSpaceViewModel @AssistedInject constructor( ) } } - CreateSpaceAction.OnBackPressed -> { + CreateSpaceAction.OnBackPressed -> { handleBackNavigation() } - CreateSpaceAction.NextFromDetails -> { + CreateSpaceAction.NextFromDetails -> { handleNextFromDetails() } - CreateSpaceAction.NextFromDefaultRooms -> { + CreateSpaceAction.NextFromDefaultRooms -> { handleNextFromDefaultRooms() } - is CreateSpaceAction.DefaultRoomNameChanged -> { + CreateSpaceAction.NextFromAdd3pid -> { + handleNextFrom3pid() + } + is CreateSpaceAction.DefaultRoomNameChanged -> { setState { copy( - defaultRooms = (defaultRooms ?: emptyMap()).toMutableMap().apply { + defaultRooms = defaultRooms.orEmpty().toMutableMap().apply { this[action.index] = action.name } ) } } - is CreateSpaceAction.SetAvatar -> { + is CreateSpaceAction.DefaultInvite3pidChanged -> { + setState { + copy( + default3pidInvite = default3pidInvite.orEmpty().toMutableMap().apply { + this[action.index] = action.email + }, + emailValidationResult = emailValidationResult.orEmpty().toMutableMap().apply { + this.remove(action.index) + } + ) + } + } + is CreateSpaceAction.SetAvatar -> { setState { copy(avatarUri = action.uri) } } - is CreateSpaceAction.SetSpaceTopology -> { + is CreateSpaceAction.SetSpaceTopology -> { handleSetTopology(action) } }.exhaustive @@ -173,20 +219,20 @@ class CreateSpaceViewModel @AssistedInject constructor( setState { copy( spaceTopology = SpaceTopology.MeAndTeammates, - step = CreateSpaceState.Step.AddRooms + step = CreateSpaceState.Step.AddEmailsOrInvites ) } - _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) + _viewEvents.post(CreateSpaceEvents.NavigateToAdd3Pid) } } } private fun handleBackNavigation() = withState { state -> when (state.step) { - CreateSpaceState.Step.ChooseType -> { + CreateSpaceState.Step.ChooseType -> { _viewEvents.post(CreateSpaceEvents.Dismiss) } - CreateSpaceState.Step.SetDetails -> { + CreateSpaceState.Step.SetDetails -> { setState { copy( step = CreateSpaceState.Step.ChooseType, @@ -196,15 +242,15 @@ class CreateSpaceViewModel @AssistedInject constructor( } _viewEvents.post(CreateSpaceEvents.NavigateToChooseType) } - CreateSpaceState.Step.AddRooms -> { + CreateSpaceState.Step.AddRooms -> { if (state.spaceType == SpaceType.Private && state.spaceTopology == SpaceTopology.MeAndTeammates) { setState { copy( spaceTopology = null, - step = CreateSpaceState.Step.ChoosePrivateType + step = CreateSpaceState.Step.AddEmailsOrInvites ) } - _viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType) + _viewEvents.post(CreateSpaceEvents.NavigateToAdd3Pid) } else { setState { copy( @@ -214,7 +260,7 @@ class CreateSpaceViewModel @AssistedInject constructor( _viewEvents.post(CreateSpaceEvents.NavigateToDetails) } } - CreateSpaceState.Step.ChoosePrivateType -> { + CreateSpaceState.Step.ChoosePrivateType -> { setState { copy( step = CreateSpaceState.Step.SetDetails @@ -222,6 +268,36 @@ class CreateSpaceViewModel @AssistedInject constructor( } _viewEvents.post(CreateSpaceEvents.NavigateToDetails) } + CreateSpaceState.Step.AddEmailsOrInvites -> { + setState { + copy( + step = CreateSpaceState.Step.ChoosePrivateType + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType) + } + } + } + + private fun handleNextFrom3pid() = withState { state -> + // check if emails are valid + val emailValidation = state.default3pidInvite?.mapValues { + val email = it.value + email.isNullOrEmpty() || email.isEmail() + } + if (emailValidation?.all { it.value } != false) { + setState { + copy( + step = CreateSpaceState.Step.AddRooms + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) + } else { + setState { + copy( + emailValidationResult = emailValidation + ) + } } } @@ -296,12 +372,18 @@ class CreateSpaceViewModel @AssistedInject constructor( defaultRooms = state.defaultRooms ?.entries ?.sortedBy { it.key } - ?.mapNotNull { it.value } ?: emptyList(), - spaceAlias = alias + ?.mapNotNull { it.value } + .orEmpty(), + spaceAlias = alias, + defaultEmailToInvite = state.default3pidInvite + ?.values + ?.mapNotNull { it.takeIf { it?.isEmail() == true } } + ?.takeIf { state.spaceTopology == SpaceTopology.MeAndTeammates } + .orEmpty() ) ) when (result) { - is CreateSpaceTaskResult.Success -> { + is CreateSpaceTaskResult.Success -> { setState { copy(creationResult = Success(result.spaceId)) } 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 c68b8a0b9b..f6f168c365 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 @@ -25,12 +25,19 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility 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 import javax.inject.Inject @@ -49,7 +56,8 @@ data class CreateSpaceTaskParams( val spaceAvatar: Uri? = null, val spaceAlias: String? = null, val isPublic: Boolean, - val defaultRooms: List = emptyList() + val defaultRooms: List = emptyList(), + val defaultEmailToInvite: List = emptyList() ) class CreateSpaceViewModelTask @Inject constructor( @@ -60,13 +68,31 @@ class CreateSpaceViewModelTask @Inject constructor( override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult { val spaceID = try { - session.spaceService().createSpace( - params.spaceName, - params.spaceTopic, - params.spaceAvatar, - params.isPublic, - params.spaceAlias - ) + session.spaceService().createSpace(CreateSpaceParams().apply { + this.name = params.spaceName + this.topic = params.spaceTopic + this.avatarUri = params.spaceAvatar + if (params.isPublic) { + this.roomAliasName = params.spaceAlias + this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( + invite = Role.Default.value + ) + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE + this.guestAccess = GuestAccess.CanJoin + } else { + this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT + visibility = RoomDirectoryVisibility.PRIVATE + this.invite3pids.addAll( + params.defaultEmailToInvite.map { + ThreePid.Email(it) + } + ) + this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( + invite = Role.Moderator.value + ) + } + }) } catch (failure: Throwable) { return CreateSpaceTaskResult.FailedToCreateSpace(failure) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt new file mode 100644 index 0000000000..05d8a78b30 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt @@ -0,0 +1,99 @@ +/* + * 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.spaces.create + +import android.text.InputType +import com.airbnb.epoxy.TypedEpoxyController +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.ItemStyle +import im.vector.app.core.ui.list.genericButtonItem +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.core.ui.list.genericPillItem +import im.vector.app.features.form.formEditTextItem +import javax.inject.Inject + +class SpaceAdd3pidEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: CreateSpaceState?) { + val host = this + data ?: return + genericFooterItem { + id("info_help_header") + style(ItemStyle.TITLE) + text(host.stringProvider.getString(R.string.create_spaces_invite_public_header)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + } + genericFooterItem { + id("info_help_desc") + text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: "")) + textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) + } + + if (data.canInviteByMail) { + buildEmailFields(data, host) + } else { + genericPillItem { + id("no_IDS") + imageRes(R.drawable.ic_baseline_perm_contact_calendar_24) + text(host.stringProvider.getString(R.string.create_space_identity_server_info_none)) + } + genericButtonItem { + id("Discover_Settings") + text(host.stringProvider.getString(R.string.open_discovery_settings)) + textColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + buttonClickAction { + host.listener?.onNoIdentityServer() + } + } + } + } + + private fun buildEmailFields(data: CreateSpaceState, host: SpaceAdd3pidEpoxyController) { + for (index in 0..2) { + val mail = data.default3pidInvite?.get(index) + formEditTextItem { + id("3pid$index") + enabled(true) + value(mail) + hint(host.stringProvider.getString(R.string.medium_email)) + inputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) + endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) + errorMessage( + if (data.emailValidationResult?.get(index) == false) { + host.stringProvider.getString(R.string.does_not_look_like_valid_email) + } else null + ) + onTextChange { text -> + host.listener?.on3pidChange(index, text) + } + } + } + } + + interface Listener { + fun on3pidChange(index: Int, newName: String) + fun onNoIdentityServer() + } +} 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 ade2b4b00c..fc06ac4f7e 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 @@ -34,7 +34,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib private val views: ViewSpaceTypeButtonBinding - var title: String? = null + var title: CharSequence? = null set(value) { if (value != title) { field = value 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 63813042b2..3af66cbb6b 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 @@ -44,6 +44,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.matrixto.SpaceCardRenderer 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 @@ -75,6 +76,27 @@ class SpaceDirectoryFragment @Inject constructor( private val viewModel by activityViewModel(SpaceDirectoryViewModel::class) private val epoxyVisibilityTracker = EpoxyVisibilityTracker() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + childFragmentManager.setFragmentResultListener(SpaceAddRoomSpaceChooserBottomSheet.REQUEST_KEY, this) { _, bundle -> + + bundle.getString(SpaceAddRoomSpaceChooserBottomSheet.BUNDLE_KEY_ACTION)?.let { action -> + val spaceId = withState(viewModel) { it.spaceId } + when (action) { + SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_ROOMS -> { + addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) + } + SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_SPACES -> { + addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRoomsOnlySpaces)) + } + else -> { + // nop + } + } + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -170,7 +192,7 @@ class SpaceDirectoryFragment @Inject constructor( } override fun addExistingRooms(spaceId: String) { - addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) + SpaceAddRoomSpaceChooserBottomSheet.newInstance().show(childFragmentManager, "SpaceAddRoomSpaceChooserBottomSheet") } override fun loadAdditionalItemsIfNeeded() { 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..55b265f244 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,6 +16,7 @@ package im.vector.app.features.spaces.invite +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext @@ -32,7 +33,11 @@ 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 +47,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,6 +61,34 @@ 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? + ) + ) + ) + } } } 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 new file mode 100644 index 0000000000..88f3e1c9ff --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt @@ -0,0 +1,103 @@ +/* + * 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.spaces.leave + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.epoxy.noResultItem +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.spaces.manage.roomSelectionItem +import io.reactivex.functions.Predicate +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class SelectChildrenController @Inject constructor( + val avatarRenderer: AvatarRenderer, + val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface Listener { + fun onItemSelected(roomSummary: RoomSummary) + } + + var listener: Listener? = null + private val matchFilter = RoomSearchMatchFilter() + override fun buildModels(data: SpaceLeaveAdvanceViewState?) { + val children = data?.allChildren ?: return + val host = this + when (children) { + Uninitialized -> return + is Loading -> { + loadingItem { + id("loading") + } + } + is Success -> { + matchFilter.filter = data.currentFilter + val roomList = children.invoke().filter { matchFilter.test(it) } + + if (roomList.isEmpty()) { + noResultItem { + id("empty") + text(host.stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + roomList.forEach { item -> + roomSelectionItem { + id(item.roomId) + matrixItem(item.toMatrixItem()) + avatarRenderer(host.avatarRenderer) + selected(data.selectedRooms.contains(item.roomId)) + itemClickListener { + host.listener?.onItemSelected(item) + } + } + } + } + } + is Fail -> { +// errorWithRetryItem { +// id("failed_to_load") +// } + } + } + } + + class RoomSearchMatchFilter : Predicate { + var filter: String = "" + + override fun test(roomSummary: RoomSummary): Boolean { + if (filter.isEmpty()) { + // No filter + return true + } + // 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() + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt new file mode 100644 index 0000000000..68b313ec7f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt @@ -0,0 +1,26 @@ +/* + * 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.spaces.leave + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction { + data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction() + data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction() + object DoLeave : SpaceLeaveAdvanceViewAction() + object ClearError : SpaceLeaveAdvanceViewAction() +} 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 new file mode 100644 index 0000000000..f7802d2a31 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.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.features.spaces.leave + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class SpaceLeaveAdvanceViewState( + val spaceId: String, + val spaceSummary: RoomSummary? = null, + val allChildren: Async> = Uninitialized, + val selectedRooms: List = emptyList(), + val currentFilter: String = "", + val leaveState: Async = Uninitialized +) : MvRxState { + 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 new file mode 100644 index 0000000000..cb66708324 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -0,0 +1,134 @@ +/* + * 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.spaces.leave + +import android.content.Context +import android.content.Intent +import android.os.Bundle +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.Success +import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +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 +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleLoadingBinding +import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs +import javax.inject.Inject + +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() + super.showWaitingView(text) + } + + override fun hideWaitingView() { + views.waitingView.waitingStatusText.setTextOrHide(null) + views.waitingView.waitingHorizontalProgress.progress = 0 + views.waitingView.waitingHorizontalProgress.isVisible = false + super.hideWaitingView() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + + if (isFirstCreation()) { + val simpleName = SpaceLeaveAdvancedFragment::class.java.simpleName + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace( + R.id.simpleFragmentContainer, + SpaceLeaveAdvancedFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + } + + override fun initUiAndData() { + super.initUiAndData() + waitingView = views.waitingView.waitingView + leaveViewModel.subscribe(this) { state -> + when (state.leaveState) { + is Loading -> { + showWaitingView() + } + is Success -> { + setResult(RESULT_OK) + finish() + } + is Fail -> { + hideWaitingView() + MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(state.leaveState.error)) + .setPositiveButton(R.string.ok) { _, _ -> + leaveViewModel.handle(SpaceLeaveAdvanceViewAction.ClearError) + } + .show() + } + else -> { + hideWaitingView() + } + } + } + } + + companion object { + fun newIntent(context: Context, spaceId: String): Intent { + return Intent(context, SpaceLeaveAdvancedActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, SpaceBottomSheetSettingsArgs(spaceId)) + } + } + } + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt new file mode 100644 index 0000000000..e78d90c6d9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt @@ -0,0 +1,78 @@ +/* + * 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.spaces.leave + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.appcompat.queryTextChanges +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.FragmentSpaceLeaveAdvancedBinding +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class SpaceLeaveAdvancedFragment @Inject constructor( + val controller: SelectChildrenController +) : VectorBaseFragment(), + SelectChildrenController.Listener { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceLeaveAdvancedBinding.inflate(layoutInflater, container, false) + + val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.toolbar) + controller.listener = this + views.roomList.configureWith(controller) + views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() } + + views.spaceLeaveButton.debouncedClicks { + viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave) + } + + views.publicRoomsFilter.queryTextChanges() + .debounce(100, TimeUnit.MILLISECONDS) + .subscribeBy { + viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString())) + } + .disposeOnDestroyView() + } + + override fun onDestroyView() { + controller.listener = null + views.roomList.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + controller.setData(state) + } + + override fun onItemSelected(roomSummary: RoomSummary) { + viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt new file mode 100644 index 0000000000..7461d09b8b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt @@ -0,0 +1,141 @@ +/* + * 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.spaces.leave + +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.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.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import okhttp3.internal.toImmutableList +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.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.rx.rx +import timber.log.Timber + +class SpaceLeaveAdvancedViewModel @AssistedInject constructor( + @Assisted val initialState: SpaceLeaveAdvanceViewState, + private val session: Session, + private val appStateHandler: AppStateHandler +) : VectorViewModel(initialState) { + + override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state -> + when (action) { + is SpaceLeaveAdvanceViewAction.ToggleSelection -> { + val existing = state.selectedRooms.toMutableList() + if (existing.contains(action.roomId)) { + existing.remove(action.roomId) + } else { + existing.add(action.roomId) + } + setState { + copy( + selectedRooms = existing.toImmutableList() + ) + } + } + is SpaceLeaveAdvanceViewAction.UpdateFilter -> { + setState { copy(currentFilter = action.filter) } + } + SpaceLeaveAdvanceViewAction.DoLeave -> { + setState { copy(leaveState = Loading()) } + viewModelScope.launch { + try { + state.selectedRooms.forEach { + try { + session.getRoom(it)?.leave(null) + } catch (failure: Throwable) { + // silently ignore? + Timber.e(failure, "Fail to leave sub rooms/spaces") + } + } + + session.getRoom(initialState.spaceId)?.leave(null) + // We observe the membership and to dismiss when we have remote echo of leaving + } catch (failure: Throwable) { + setState { copy(leaveState = Fail(failure)) } + } + } + } + SpaceLeaveAdvanceViewAction.ClearError -> { + setState { copy(leaveState = Uninitialized) } + } + } + } + + init { + val spaceSummary = session.getRoomSummary(initialState.spaceId) + setState { copy(spaceSummary = spaceSummary) } + session.getRoom(initialState.spaceId)?.let { room -> + 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) + } + } + } + } + } + + viewModelScope.launch { + val children = session.getRoomSummaries( + roomSummaryQueryParams { + includeType = null + memberships = listOf(Membership.JOIN) + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(initialState.spaceId) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ) + + setState { + copy(allChildren = Success(children)) + } + } + } + + @AssistedFactory + interface Factory { + 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") + } + } +} 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 b2fb061731..e192ec3c88 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 @@ -27,6 +27,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.Loading import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.app.R @@ -109,11 +110,25 @@ class SpaceAddRoomFragment @Inject constructor( }.disposeOnDestroyView() viewModel.selectSubscribe(this, SpaceAddRoomsState::shouldShowDMs) { - dmEpoxyController.disabled = !it + dmEpoxyController.disabled = !it + }.disposeOnDestroyView() + + viewModel.selectSubscribe(this, 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 { - sharedViewModel.handle(SpaceManagedSharedAction.CreateRoom) + withState(viewModel) { state -> + if (state.onlyShowSpaces) { + sharedViewModel.handle(SpaceManagedSharedAction.CreateSpace) + } else { + sharedViewModel.handle(SpaceManagedSharedAction.CreateRoom) + } + } } viewModel.observeViewEvents { 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 new file mode 100644 index 0000000000..d0f5a9e8ba --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomSpaceChooserBottomSheet.kt @@ -0,0 +1,64 @@ +/* + * 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.spaces.manage + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.setFragmentResult +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetAddRoomsOrSpacesToSpaceBinding + +class SpaceAddRoomSpaceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override val showExpanded = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + BottomSheetAddRoomsOrSpacesToSpaceBinding.inflate(inflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.addSpaces.views.bottomSheetActionClickableZone.debouncedClicks { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_ACTION, ACTION_ADD_SPACES) + }) + dismiss() + } + + views.addRooms.views.bottomSheetActionClickableZone.debouncedClicks { + setFragmentResult(REQUEST_KEY, Bundle().apply { + putString(BUNDLE_KEY_ACTION, ACTION_ADD_ROOMS) + }) + dismiss() + } + } + + companion object { + + const val REQUEST_KEY = "SpaceAddRoomSpaceChooserBottomSheet" + const val BUNDLE_KEY_ACTION = "SpaceAddRoomSpaceChooserBottomSheet.Action" + const val ACTION_ADD_ROOMS = "Action.AddRoom" + const val ACTION_ADD_SPACES = "Action.AddSpaces" + + 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 2d9113ae68..e941d04b22 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 @@ -27,10 +27,12 @@ data class SpaceAddRoomsState( val spaceName: String = "", val ignoreRooms: List = emptyList(), val isSaving: Async> = Uninitialized, - val shouldShowDMs : Boolean = false + val shouldShowDMs: Boolean = false, + val onlyShowSpaces: Boolean = false // val selectionList: Map = emptyMap() ) : MvRxState { constructor(args: SpaceManageArgs) : this( - spaceId = args.spaceId + spaceId = args.spaceId, + onlyShowSpaces = args.manageType == ManageType.AddRoomsOnlySpaces ) } 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 35c415b087..9f5cd7d35e 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 @@ -127,7 +127,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( copy( spaceName = spaceSummary?.displayName ?: "", ignoreRooms = (spaceSummary?.flattenParentIds ?: emptyList()) + listOf(initialState.spaceId), - shouldShowDMs = spaceSummary?.isPublic == false + shouldShowDMs = !onlyShowSpaces && spaceSummary?.isPublic == false ) } } 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 630c578069..16a1e18da2 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 @@ -20,12 +20,12 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import com.google.android.material.appbar.MaterialToolbar import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragmentToBackstack @@ -40,6 +40,7 @@ import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment 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 @@ -98,7 +99,8 @@ class SpaceManageActivity : VectorBaseActivity(), if (isFirstCreation()) { withState(sharedViewModel) { when (it.manageType) { - ManageType.AddRooms -> { + ManageType.AddRooms, + ManageType.AddRoomsOnlySpaces -> { val simpleName = SpaceAddRoomFragment::class.java.simpleName if (supportFragmentManager.findFragmentByTag(simpleName) == null) { supportFragmentManager.commitTransaction { @@ -110,7 +112,7 @@ class SpaceManageActivity : VectorBaseActivity(), } } } - ManageType.Settings -> { + ManageType.Settings -> { val simpleName = SpaceSettingsFragment::class.java.simpleName if (supportFragmentManager.findFragmentByTag(simpleName) == null && args?.spaceId != null) { supportFragmentManager.commitTransaction { @@ -131,22 +133,29 @@ class SpaceManageActivity : VectorBaseActivity(), sharedViewModel.observeViewEvents { when (it) { - SpaceManagedSharedViewEvents.Finish -> { + SpaceManagedSharedViewEvents.Finish -> { finish() } - SpaceManagedSharedViewEvents.HideLoading -> { + SpaceManagedSharedViewEvents.HideLoading -> { hideWaitingView() } - SpaceManagedSharedViewEvents.ShowLoading -> { + SpaceManagedSharedViewEvents.ShowLoading -> { showWaitingView() } - SpaceManagedSharedViewEvents.NavigateToCreateRoom -> { + SpaceManagedSharedViewEvents.NavigateToCreateRoom -> { addFragmentToBackstack( R.id.simpleFragmentContainer, CreateRoomFragment::class.java, CreateRoomArgs("", parentSpaceId = args?.spaceId) ) } + SpaceManagedSharedViewEvents.NavigateToCreateSpace -> { + addFragmentToBackstack( + R.id.simpleFragmentContainer, + CreateRoomFragment::class.java, + CreateRoomArgs("", parentSpaceId = args?.spaceId, isSpace = true) + ) + } SpaceManagedSharedViewEvents.NavigateToManageRooms -> { args?.spaceId?.let { spaceId -> addFragmentToBackstack( @@ -156,7 +165,7 @@ class SpaceManageActivity : VectorBaseActivity(), ) } } - SpaceManagedSharedViewEvents.NavigateToAliasSettings -> { + SpaceManagedSharedViewEvents.NavigateToAliasSettings -> { args?.spaceId?.let { spaceId -> addFragmentToBackstack( R.id.simpleFragmentContainer, @@ -165,6 +174,14 @@ class SpaceManageActivity : VectorBaseActivity(), ) } } + SpaceManagedSharedViewEvents.NavigateToPermissionSettings -> { + args?.spaceId?.let { spaceId -> + addFragmentToBackstack( + R.id.simpleFragmentContainer, RoomPermissionsFragment::class.java, + RoomProfileArgs(spaceId) + ) + } + } } } } 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 0222dde275..b1f6d5c3c3 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 @@ -33,6 +33,7 @@ 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 class SpaceManageRoomsViewModel @AssistedInject constructor( @@ -104,6 +105,10 @@ class SpaceManageRoomsViewModel @AssistedInject constructor( } catch (failure: Throwable) { errorList.add(failure) } + tryOrNull { + // also remove space parent if any? and if I can + session.spaceService().removeSpaceParent(it, state.spaceId) + } } if (errorList.isEmpty()) { // success 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 f1d041056f..8f23788d19 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 @@ -23,6 +23,7 @@ import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session @@ -48,15 +49,17 @@ class SpaceManageSharedViewModel @AssistedInject constructor( override fun handle(action: SpaceManagedSharedAction) { when (action) { - SpaceManagedSharedAction.HandleBack -> { + SpaceManagedSharedAction.HandleBack -> { // for now finish _viewEvents.post(SpaceManagedSharedViewEvents.Finish) } - SpaceManagedSharedAction.HideLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.HideLoading) - SpaceManagedSharedAction.ShowLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.ShowLoading) - SpaceManagedSharedAction.CreateRoom -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateRoom) - SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms) - SpaceManagedSharedAction.OpenSpaceAliasesSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToAliasSettings) - } + SpaceManagedSharedAction.HideLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.HideLoading) + SpaceManagedSharedAction.ShowLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.ShowLoading) + SpaceManagedSharedAction.CreateRoom -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateRoom) + SpaceManagedSharedAction.CreateSpace -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateSpace) + SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms) + SpaceManagedSharedAction.OpenSpaceAliasesSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToAliasSettings) + SpaceManagedSharedAction.OpenSpacePermissionSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToPermissionSettings) + }.exhaustive } } 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 91d49d90d1..35596f0884 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 @@ -20,6 +20,7 @@ import com.airbnb.mvrx.MvRxState enum class ManageType { AddRooms, + AddRoomsOnlySpaces, Settings, ManageRooms } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt index 77143470bc..5f2032b25d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt @@ -23,6 +23,8 @@ sealed class SpaceManagedSharedAction : VectorViewModelAction { object ShowLoading : SpaceManagedSharedAction() object HideLoading : SpaceManagedSharedAction() object CreateRoom : SpaceManagedSharedAction() + object CreateSpace : SpaceManagedSharedAction() object ManageRooms : SpaceManagedSharedAction() object OpenSpaceAliasesSettings : SpaceManagedSharedAction() + object OpenSpacePermissionSettings : SpaceManagedSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt index ab993764c6..789f107341 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt @@ -23,6 +23,8 @@ sealed class SpaceManagedSharedViewEvents : VectorViewEvents { object ShowLoading : SpaceManagedSharedViewEvents() object HideLoading : SpaceManagedSharedViewEvents() object NavigateToCreateRoom : SpaceManagedSharedViewEvents() + object NavigateToCreateSpace : SpaceManagedSharedViewEvents() object NavigateToManageRooms : SpaceManagedSharedViewEvents() object NavigateToAliasSettings : SpaceManagedSharedViewEvents() + object NavigateToPermissionSettings : SpaceManagedSharedViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt index 27204be8a6..7cd5a70279 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -25,7 +25,6 @@ import im.vector.app.core.resources.StringProvider 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.formSwitchItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.settings.RoomSettingsViewState import im.vector.app.features.settings.VectorPreferences @@ -53,6 +52,7 @@ class SpaceSettingsController @Inject constructor( fun onManageRooms() fun setIsPublic(public: Boolean) fun onRoomAliasesClicked() + fun onRoomPermissionsClicked() } var callback: Callback? = null @@ -105,27 +105,35 @@ class SpaceSettingsController @Inject constructor( } val isPublic = (data.newRoomJoinRules.newJoinRules ?: data.currentRoomJoinRules) == RoomJoinRules.PUBLIC - if (vectorPreferences.labsUseExperimentalRestricted()) { - buildProfileAction( - id = "joinRule", - title = stringProvider.getString(R.string.room_settings_room_access_title), - subtitle = data.getJoinRuleWording(stringProvider), - divider = false, - editable = data.actionPermissions.canChangeJoinRule, - action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } - ) - } else { - formSwitchItem { - id("isPublic") - enabled(data.actionPermissions.canChangeJoinRule) - title(host.stringProvider.getString(R.string.make_this_space_public)) - switchChecked(isPublic) - - listener { value -> - host.callback?.setIsPublic(value) - } - } - } + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_space_access_title), + subtitle = data.getJoinRuleWording(stringProvider), + divider = true, + editable = data.actionPermissions.canChangeJoinRule, + action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } + ) +// if (vectorPreferences.labsUseExperimentalRestricted()) { +// buildProfileAction( +// id = "joinRule", +// title = stringProvider.getString(R.string.room_settings_room_access_title), +// subtitle = data.getJoinRuleWording(stringProvider), +// divider = false, +// editable = data.actionPermissions.canChangeJoinRule, +// action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } +// ) +// } else { +// formSwitchItem { +// id("isPublic") +// enabled(data.actionPermissions.canChangeJoinRule) +// title(host.stringProvider.getString(R.string.make_this_space_public)) +// switchChecked(isPublic) +// +// listener { value -> +// host.callback?.setIsPublic(value) +// } +// } +// } dividerItem { id("divider") } @@ -134,7 +142,7 @@ class SpaceSettingsController @Inject constructor( id = "manage_rooms", title = stringProvider.getString(R.string.space_settings_manage_rooms), // subtitle = data.getJoinRuleWording(stringProvider), - divider = vectorPreferences.developerMode() || isPublic, + divider = true, editable = data.actionPermissions.canAddChildren, action = { if (data.actionPermissions.canAddChildren) callback?.onManageRooms() @@ -146,12 +154,21 @@ class SpaceSettingsController @Inject constructor( id = "alias", title = stringProvider.getString(R.string.space_settings_alias_title), subtitle = stringProvider.getString(R.string.space_settings_alias_subtitle), - divider = vectorPreferences.developerMode(), + divider = true, editable = true, action = { callback?.onRoomAliasesClicked() } ) } + buildProfileAction( + id = "permissions", + title = stringProvider.getString(R.string.space_settings_permissions_title), + subtitle = stringProvider.getString(R.string.space_settings_permissions_subtitle), + divider = vectorPreferences.developerMode(), + editable = true, + action = { callback?.onRoomPermissionsClicked() } + ) + if (vectorPreferences.developerMode()) { buildProfileAction( id = "dev_tools", 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 e831732bcc..5e5eb50b87 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 @@ -47,7 +47,7 @@ import im.vector.app.features.roomprofile.settings.RoomSettingsAction import im.vector.app.features.roomprofile.settings.RoomSettingsViewEvents import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel import im.vector.app.features.roomprofile.settings.RoomSettingsViewState -import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -133,12 +133,6 @@ class SpaceSettingsFragment @Inject constructor( state.roomSummary()?.let { views.roomSettingsToolbarTitleView.text = it.displayName - views.roomSettingsToolbarTitleView.setCompoundDrawablesWithIntrinsicBounds( - null, - null, - drawableProvider.getDrawable(R.drawable.ic_beta_pill), - null - ) avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } @@ -199,10 +193,8 @@ class SpaceSettingsFragment @Inject constructor( // N/A for space settings screen } - override fun onJoinRuleClicked() = withState(viewModel) { state -> - val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules - RoomJoinRuleBottomSheet.newInstance(currentJoinRule) - .show(childFragmentManager, "RoomJoinRuleBottomSheet") + override fun onJoinRuleClicked() { + startActivity(RoomJoinRuleActivity.newIntent(requireContext(), roomProfileArgs.roomId)) } override fun onToggleGuestAccess() = withState(viewModel) { state -> @@ -237,6 +229,10 @@ class SpaceSettingsFragment @Inject constructor( sharedViewModel.handle(SpaceManagedSharedAction.OpenSpaceAliasesSettings) } + override fun onRoomPermissionsClicked() { + sharedViewModel.handle(SpaceManagedSharedAction.OpenSpacePermissionSettings) + } + override fun onImageReady(uri: Uri?) { uri ?: return viewModel.handle( 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..bd69de0d95 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 @@ -73,7 +73,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment "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/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/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..ba740f8556 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,13 @@ 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.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 +41,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 +91,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 +314,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..f251a672b8 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,9 +39,11 @@ 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.settings.VectorSettingsActivity import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User @@ -131,7 +133,7 @@ class UserListFragment @Inject constructor( private fun setupSearchView() { withState(viewModel) { - views.userListSearch.hint = getString(R.string.user_directory_search_hint) + views.userListSearch.hint = getString(R.string.user_directory_search_hint_2) } views.userListSearch .textChanges() @@ -217,6 +219,21 @@ 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) { + 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..dead957795 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 @@ -19,18 +19,22 @@ 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.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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 org.matrix.android.sdk.api.MatrixPatterns 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 @@ -41,12 +45,18 @@ 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 knownUsersSearch = BehaviorRelay.create() private val directoryUsersSearch = BehaviorRelay.create() + private val identityServerUsersSearch = BehaviorRelay.create() @AssistedFactory interface Factory { @@ -64,24 +74,72 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } } + private val identityServerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() { + withState { + identityServerUsersSearch.accept(it.searchTerm) + setState { + copy( + configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl()) + ) + } + } + } + } + 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.accept(it.searchTerm) + } + } + private fun handleSearchUsers(searchTerm: String) { setState { - copy(searchTerm = searchTerm) + copy( + searchTerm = 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.accept(searchTerm) knownUsersSearch.accept(searchTerm) directoryUsersSearch.accept(searchTerm) } @@ -95,12 +153,45 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User private fun handleClearSearchUsers() { knownUsersSearch.accept("") directoryUsersSearch.accept("") + identityServerUsersSearch.accept("") setState { copy(searchTerm = "") } } private fun observeUsers() = withState { state -> + + identityServerUsersSearch + .filter { it.isEmail() } + .throttleLast(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + val rx = session.rx() + val stream = + rx.lookupThreePid(ThreePid.Email(search)).flatMap { + it.getOrNull()?.let { foundThreePid -> + rx.getProfileInfo(foundThreePid.matrixId) + .map { json -> + ThreePidUser( + email = search, + user = User( + userId = foundThreePid.matrixId, + displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String, + avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String + ) + ) + } + .onErrorResumeNext { + Single.just(ThreePidUser(email = search, user = User(foundThreePid.matrixId))) + } + } ?: Single.just(ThreePidUser(email = search, user = null)) + } + stream.toAsync { + copy(matchingEmail = it) + } + } + .subscribe() + .disposeOnClear() + knownUsersSearch .throttleLast(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -136,14 +227,16 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String ).toOptional() } - .onErrorReturn { + .onErrorResumeNext { // 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() + Single.just( + User( + userId = search, + displayName = null, + avatarUrl = null + ).toOptional() + ) } Single.zip( 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..b66d36c5f0 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 @@ -27,10 +27,12 @@ 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 { diff --git a/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml b/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml new file mode 100644 index 0000000000..e80a55bc16 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_leave_space.xml b/vector/src/main/res/layout/bottom_sheet_leave_space.xml new file mode 100644 index 0000000000..b9626bf785 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_leave_space.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + +