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 878e9bd84a..d0606f40c2 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 2bfd8b4ee0..3360ab0ea1 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..9ff90c7264
--- /dev/null
+++ b/dependencies.gradle
@@ -0,0 +1,132 @@
+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",
+ 'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$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",
+ 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$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"
+ ]
+]
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 9282f3c84e..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=-Xmx4096m
-# 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 faacaeb3e9..ed2c0526a3 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
@@ -258,4 +258,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 1d2cb3d569..253af51960 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
@@ -226,63 +228,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 3ce97f75e6..7a795fea23 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
ext.scVersion = 44
@@ -104,17 +104,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 "de.spiritcroc.riotx"
// Set to API 21: see #405
- minSdkVersion 21
- targetSdkVersion 30
+ minSdk versions.minSdk
+ targetSdk versions.targetSdk
multiDexEnabled true
renderscriptTargetApi 24
@@ -294,8 +297,8 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility versions.sourceCompat
+ targetCompatibility versions.targetCompat
}
kotlinOptions {
@@ -322,26 +325,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")
@@ -350,75 +333,79 @@ 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.kotlin:kotlin-reflect:$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.kotlinReflect
+ 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 "com.squareup.moshi:moshi-kotlin:$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
+ implementation libs.squareup.moshiKt
+ 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'
@@ -433,7 +420,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'
@@ -442,16 +429,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'
@@ -462,8 +449,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
// UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:1.2.0'
@@ -505,36 +492,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/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 20557f4918..cd45699c4c 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -330,6 +330,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 21e43dddef..718bfcb793 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
@@ -62,6 +62,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(context: Context, pushKey: String) {
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
currentSession.removeHttpPusher(pushKey, getPusherAppId(context))
diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
index 09e9a7e8e4..d3bc2ba51e 100755
--- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt
@@ -40,12 +40,14 @@ import im.vector.app.features.notifications.NotifiableEventResolver
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.settings.BackgroundSyncMode
+import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences
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.session.Session
import org.unifiedpush.android.connector.MessagingReceiver
import org.unifiedpush.android.connector.MessagingReceiverHandler
@@ -69,6 +71,8 @@ data class Counts(
val unread: Int = 0
)
+private val loggerTag = LoggerTag("Push", LoggerTag.SYNC)
+
/**
* UnifiedPush handler.
*/
@@ -79,6 +83,7 @@ val upHandler = object: MessagingReceiverHandler {
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())
@@ -95,6 +100,7 @@ val upHandler = object: MessagingReceiverHandler {
pusherManager = pusherManager()
activeSessionHolder = activeSessionHolder()
vectorPreferences = vectorPreferences()
+ vectorDataStore = vectorDataStore()
wifiDetector = wifiDetector()
}
}
@@ -108,9 +114,14 @@ val upHandler = object: MessagingReceiverHandler {
override fun onMessage(context: Context?, message: String, instance: String) {
initVar(context!!)
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
- Timber.d("## onMessageReceived() %s", message)
+ Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message)
+ } else {
+ Timber.tag(loggerTag.value).d("## onMessageReceived()")
+ }
+
+ runBlocking {
+ vectorDataStore.incrementPushCounter()
}
- Timber.d("## onMessage() received")
val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
@@ -127,6 +138,7 @@ val upHandler = object: MessagingReceiverHandler {
notification.unread = notification.counts.unread
}
+
// Diagnostic Push
if (notification.eventId == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION)
@@ -135,14 +147,14 @@ val upHandler = object: MessagingReceiverHandler {
}
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(context, notification)
}
@@ -157,7 +169,7 @@ val upHandler = object: MessagingReceiverHandler {
*/
override fun onNewEndpoint(context: Context?, endpoint: String, instance: String) {
initVar(context!!)
- Timber.i("onNewEndpoint: adding $endpoint")
+ Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
val gateway = UPHelper.customOrDefaultGateway(context, endpoint)
if (UPHelper.getUpEndpoint(context) != endpoint
@@ -166,7 +178,7 @@ val upHandler = object: MessagingReceiverHandler {
UPHelper.storeUpEndpoint(context, endpoint)
pusherManager.registerPusher(context, endpoint, gateway)
} else {
- Timber.i("onNewEndpoint: skipped")
+ Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
}
}
if (!UPHelper.allowBackgroundSync(context)) {
@@ -184,7 +196,7 @@ val upHandler = object: MessagingReceiverHandler {
}
override fun onUnregistered(context: Context?, instance: String) {
- Timber.d("Unifiedpush: Unregistered")
+ Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
initVar(context!!)
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY
vectorPreferences.setFdroidSyncBackgroundMode(mode)
@@ -192,7 +204,7 @@ val upHandler = object: MessagingReceiverHandler {
try {
pusherManager.unregisterPusher(context, UPHelper.getUpEndpoint(context)!!)
} catch (e: Exception) {
- Timber.d("Probably unregistering a non existant pusher")
+ Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher")
}
}
}
@@ -205,7 +217,9 @@ val upHandler = object: MessagingReceiverHandler {
private fun onMessageReceivedInternal(context: Context, notification: Notification) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
- Timber.d("## onMessageReceivedInternal() : $notification")
+ Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification")
+ } else {
+ Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")
}
// update the badge counter
@@ -214,21 +228,21 @@ val upHandler = object: MessagingReceiverHandler {
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 {
if (isEventAlreadyKnown(notification.eventId, notification.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, notification.roomId, notification.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")
}
}
@@ -242,18 +256,18 @@ val upHandler = object: MessagingReceiverHandler {
}
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)
@@ -271,7 +285,7 @@ val upHandler = object: MessagingReceiverHandler {
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
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 fb03625bd5..ae5f09571c 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.core.resources.ColorProvider
import im.vector.app.databinding.ViewBottomSheetActionButtonBinding
@@ -81,7 +82,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
@@ -96,6 +97,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)
@@ -110,6 +117,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 cdcf408911..93ecc29aec 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 d68cc706dd..751891b9a5 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
@@ -72,7 +72,7 @@ import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import im.vector.app.core.pushers.UPHelper
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
@@ -83,7 +83,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 :
@@ -231,6 +232,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) {
@@ -304,11 +310,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 {
@@ -326,6 +332,7 @@ class HomeActivity :
}
views.waitingView.root.isVisible = true
}
+ else -> Unit
}.exhaustive
}
@@ -424,9 +431,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)
}
@@ -462,8 +477,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)
}
@@ -552,10 +567,15 @@ class HomeActivity :
}
companion object {
- fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
+ fun newIntent(context: Context,
+ clearNotification: Boolean = false,
+ accountCreation: Boolean = false,
+ inviteNotificationRoomId: String? = null
+ ): Intent {
val args = HomeActivityArgs(
clearNotification = clearNotification,
- accountCreation = accountCreation
+ accountCreation = accountCreation,
+ inviteNotificationRoomId = inviteNotificationRoomId
)
return Intent(context, HomeActivity::class.java)
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 c5eeb262c4..652418a554 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
@@ -70,7 +71,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,
@@ -218,6 +220,18 @@ class HomeDetailFragment @Inject constructor(
//updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
checkNotificationTabStatus()
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 checkNotificationTabStatus(enableDialPad: Boolean? = null) {
@@ -503,7 +517,11 @@ class HomeDetailFragment @Inject constructor(
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.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_all).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 76ecda3c1b..0b1816b00c 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
@@ -34,13 +34,16 @@ import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.home.room.ScSdkPreferences
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
@@ -58,10 +61,11 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private val session: Session,
private val uiStateRepository: UiStateRepository,
private val scSdkPreferences: ScSdkPreferences,
+ 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 {
@@ -91,6 +95,7 @@ private val autoAcceptInvites: AutoAcceptInvites)
observeRoomGroupingMethod()
observeRoomSummaries()
updateShowDialPadTab()
+ observeDataStore()
callManager.addProtocolsCheckerListener(this)
session.rx().liveUser(session.myUserId).execute {
copy(
@@ -99,6 +104,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)
@@ -175,6 +192,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 aa3f592b6b..459e7262df 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 066746a361..0616fbcbc6 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
@@ -39,6 +39,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
@@ -148,7 +149,19 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
).size
}
+
val spacesShowAllRoomsInHome = vectorPreferences.prefSpacesShowAllRoomInHome()
+
+ 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)
@@ -181,8 +194,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
}
val topLevelCounts = RoomAggregateNotificationCount(
- topLevelTotalCount.notificationCount + inviteCount,
- topLevelTotalCount.highlightCount + inviteCount,
+ topLevelTotalCount.notificationCount + inviteCount + spaceInviteCount,
+ topLevelTotalCount.highlightCount + inviteCount + spaceInviteCount,
topLevelTotalCount.unreadCount,
topLevelTotalCount.markedUnreadCount
)
@@ -195,22 +208,22 @@ 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,
- rootCounts.fold(0, { acc, rs ->
- acc + (if (rs.scIsUnread(scSdkPreferences)) 1 else 0)
- }) + (counts.unreadCount.takeIf { selectedSpace != null } ?: 0),
- rootCounts.fold(0, { acc, rs ->
- acc + (if (rs.markedUnread) 1 else 0)
- }) + (counts.markedUnreadCount.takeIf { selectedSpace != null } ?: 0)
+ unreadCount = rootCounts.fold(0, { acc, rs -> acc + (if (rs.scIsUnread(scSdkPreferences)) 1 else 0) })
+ + (counts.unreadCount.takeIf { selectedSpace != null } ?: 0)
+ + spaceInviteCount,
+ markedUnreadCount = rootCounts.fold(0, { acc, rs -> acc + (if (rs.markedUnread) 1 else 0) })
+ + (counts.markedUnreadCount.takeIf { selectedSpace != null } ?: 0)
)
)
*/
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 608cc50c57..3d2b5c139e 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
@@ -153,6 +155,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
@@ -186,7 +189,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
@@ -316,7 +318,10 @@ class RoomDetailFragment @Inject constructor(
private var lockSendButton = false
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
- private lateinit var emojiPopup: EmojiPopup
+ private val lazyLoadedViews = RoomDetailLazyLoadedViews()
+ private val emojiPopup: EmojiPopup by lifecycleAwareLazy {
+ createEmojiPopup()
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -328,7 +333,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)
@@ -344,16 +349,15 @@ class RoomDetailFragment @Inject constructor(
onTapToReturnToCall = ::onTapToReturnToCall
)
keyboardStateUtils = KeyboardStateUtils(requireActivity())
+ lazyLoadedViews.bind(views)
setupToolbar(views.roomToolbar)
setupRecyclerView()
setupComposer()
- setupInviteView()
setupNotificationView()
setupJumpToReadMarkerView()
setupActiveCallView()
setupJumpToBottomView()
- setupEmojiPopup()
- setupFailedMessagesWarningView()
+ setupEmojiButton()
setupRemoveJitsiWidgetView()
setupVoiceMessageView()
@@ -370,10 +374,10 @@ class RoomDetailFragment @Inject constructor(
knownCallsViewModel
.liveKnownCalls
- .observe(viewLifecycleOwner, {
+ .observe(viewLifecycleOwner) {
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it)
invalidateOptionsMenu()
- })
+ }
roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
updateJumpToReadMarkerViewVisibility()
@@ -391,8 +395,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 {
@@ -458,11 +471,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() {
@@ -588,8 +601,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)
@@ -606,14 +625,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)
@@ -631,14 +654,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
@@ -771,6 +786,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onDestroyView() {
+ lazyLoadedViews.unBind()
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
currentCallsViewPresenter.unBind()
@@ -778,8 +794,6 @@ class RoomDetailFragment @Inject constructor(
autoCompleter.clear()
debouncer.cancelAll()
views.timelineRecyclerView.cleanup()
- emojiPopup.dismiss()
-
super.onDestroyView()
}
@@ -1385,22 +1399,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.scIsUnread(ScSdkPreferences(context))
timelineEventController.update(state)
- views.inviteView.isVisible = false
+ lazyLoadedViews.inviteView(false)?.isVisible = false
if (state.tombstoneEvent == null) {
if (state.canSendMessage) {
if (!views.voiceMessageRecorderView.isActive()) {
@@ -1421,10 +1435,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()
}
@@ -1748,7 +1767,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 acbc9cf334..733d26dfcf 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,6 +57,7 @@ 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
@@ -63,11 +65,11 @@ import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
+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
@@ -81,6 +83,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
@@ -103,6 +106,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
@@ -112,6 +116,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,
@@ -180,6 +185,7 @@ class RoomDetailViewModel @AssistedInject constructor(
observeSummaryState()
getUnreadState()
observeSyncState()
+ observeDataStore()
observeEventDisplayedActions()
loadDraftIfAny()
observeUnreadState()
@@ -208,6 +214,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) {
@@ -380,12 +398,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
}
}
@@ -1511,6 +1529,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 d0995fe776..c6ce6c8da7 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
@@ -79,6 +80,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 2875599f36..c9bda5aabe 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,7 +39,6 @@ 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.InvalidateTimelineEventDiffUtilCallback
@@ -48,6 +47,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineControlle
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
+import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
@@ -298,6 +298,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 63820b1a09..051dd818c3 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
@@ -467,7 +467,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)
@@ -681,7 +680,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 86449cac8a..edac7a39a6 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
@@ -47,8 +47,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()
}
@@ -87,7 +87,9 @@ class RoomMemberProfileController @Inject constructor(
}
private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {
- buildSecuritySection(state)
+ if (!state.isSpace) {
+ buildSecuritySection(state)
+ }
buildMoreSection(state)
buildAdminSection(state)
}
@@ -185,7 +187,7 @@ class RoomMemberProfileController @Inject constructor(
action = { callback?.onOpenDmClicked() }
)
- if (state.hasReadReceipt) {
+ if (!state.isSpace && state.hasReadReceipt) {
buildProfileAction(
id = "read_receipt",
editable = false,
@@ -195,16 +197,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",
@@ -265,7 +269,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 -> {
@@ -292,7 +296,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 4fc85ffc0c..c527dd816e 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
@@ -120,6 +120,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"
@@ -162,6 +163,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"
@@ -334,6 +336,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 91bb37ab8d..bb411f1408 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
@@ -25,7 +25,10 @@ import android.os.Handler
import android.os.Looper
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
@@ -45,11 +48,14 @@ import im.vector.app.features.settings.BackgroundSyncModeChooserDialog
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.features.settings.VectorSettingsLabsFragment
+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 timber.log.Timber
import javax.inject.Inject
@@ -142,11 +148,51 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
true
}
+ 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
}
@@ -387,3 +433,43 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
const val SETTINGS_UNIFIED_PUSH_RE_REGISTER = "SETTINGS_UNIFIED_PUSH_RE_REGISTER"
}
}
+
+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 57492427dc..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)
@@ -155,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 {
@@ -200,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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/bottom_sheet_space_create_private_warning.xml b/vector/src/main/res/layout/bottom_sheet_space_create_private_warning.xml
deleted file mode 100644
index 6e46c87548..0000000000
--- a/vector/src/main/res/layout/bottom_sheet_space_create_private_warning.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/bottom_sheet_space_invite.xml b/vector/src/main/res/layout/bottom_sheet_space_invite.xml
index 03893a45f9..1fa132a086 100644
--- a/vector/src/main/res/layout/bottom_sheet_space_invite.xml
+++ b/vector/src/main/res/layout/bottom_sheet_space_invite.xml
@@ -34,14 +34,14 @@
app:layout_constraintVertical_bias="1"
tools:text="@string/invite_people_to_your_space_desc" />
-
+
+
+
+
+
+
+
+
+ app:title="@string/invite_by_mxid_or_mail" />
-
+
-
-
-
+ app:layout_constraintStart_toStartOf="parent" />
-
+ app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
+ app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningStub" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_generic_pill_footer.xml b/vector/src/main/res/layout/item_generic_pill_footer.xml
index ba883ebf68..4084f549a1 100644
--- a/vector/src/main/res/layout/item_generic_pill_footer.xml
+++ b/vector/src/main/res/layout/item_generic_pill_footer.xml
@@ -12,7 +12,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_pushgateway.xml b/vector/src/main/res/layout/item_pushgateway.xml
index 28bcef1ee2..d3b1f3ed76 100644
--- a/vector/src/main/res/layout/item_pushgateway.xml
+++ b/vector/src/main/res/layout/item_pushgateway.xml
@@ -120,7 +120,6 @@
android:text="@string/push_gateway_item_format"
android:textStyle="bold" />
-
+
diff --git a/vector/src/main/res/layout/item_space_beta_header.xml b/vector/src/main/res/layout/item_space_beta_header.xml
index 1da37e6c4c..ebc6454ff0 100644
--- a/vector/src/main/res/layout/item_space_beta_header.xml
+++ b/vector/src/main/res/layout/item_space_beta_header.xml
@@ -15,8 +15,7 @@
android:layout_marginTop="8dp"
android:text="@string/spaces_header"
android:textColor="?vctr_content_primary"
- android:textStyle="bold"
- app:drawableEndCompat="@drawable/ic_beta_pill" />
+ android:textStyle="bold" />
+ android:gravity="center"
+ tools:parentTag="android.widget.LinearLayout">
@@ -15,25 +16,6 @@
-
-
-
-
diff --git a/vector/src/main/res/layout/view_bottom_sheet_action_button.xml b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml
index 77a4d6bc57..30277f4b70 100644
--- a/vector/src/main/res/layout/view_bottom_sheet_action_button.xml
+++ b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml
@@ -45,7 +45,7 @@
android:textColor="?colorPrimary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/bottomSheetActionSubTitle"
- app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionIcon"
+ app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionBeta"
app:layout_constraintStart_toEndOf="@+id/bottomSheetActionLeftIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
@@ -62,12 +62,23 @@
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionIcon"
+ app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionBeta"
app:layout_constraintStart_toStartOf="@+id/bottomSheetActionTitle"
app:layout_constraintTop_toBottomOf="@+id/bottomSheetActionTitle"
tools:text="For maximum security, do this in person"
tools:visibility="visible" />
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/view_stub_invite_layout.xml b/vector/src/main/res/layout/view_stub_invite_layout.xml
new file mode 100644
index 0000000000..29cabce86d
--- /dev/null
+++ b/vector/src/main/res/layout/view_stub_invite_layout.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/view_sync_state.xml b/vector/src/main/res/layout/view_sync_state.xml
index 55b8d4524a..38d2a5bfc3 100644
--- a/vector/src/main/res/layout/view_sync_state.xml
+++ b/vector/src/main/res/layout/view_sync_state.xml
@@ -7,6 +7,34 @@
tools:orientation="vertical"
tools:parentTag="android.widget.LinearLayout">
+
+
+
+
+
+
+
+
استكشِف الغُرفأضف غُرفغادر المساحة
+
هل أنت متأكد أنك تريد مغادرة المساحة؟انت الشخص الوحيد هنا إذا غادرت، فلن يتمكن أي شخص من الانضمام في المستقبل، بما في ذلك أنت.هذه المساحة ليست عامة. لن تتمكن من الانضمام مرة أخرى بدون دعوة.
diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml
index ca31bad408..99b8f912f4 100644
--- a/vector/src/main/res/values-cs/strings.xml
+++ b/vector/src/main/res/values-cs/strings.xml
@@ -1573,7 +1573,7 @@
Jiná oznámení třetích stranJiž se díváte do této místnosti!Rychlé reakce
- Obecná
+ ObecnéMožnostiZabezpečení a soukromíExpert
@@ -2726,17 +2726,18 @@
Zpráva odeslánaNeoznačenoExperimentální, prostor - omezená místnost.
- Prostory představují nový způsob seskupování místností a osob.
+ Spaces představují nový způsob seskupování místností a osob.Založme pro každé místnost. Později můžete přidat i další, včetně již existujících.Na jakých tématech pracujete\?Založíme pro ně místnosti. Můžete přidat další později.Doplňte nějaké podrobnosti, aby jej lidé mohli identifikovat. Můžete je kdykoli změnit.
- Prostory jsou nový způsob organizace místností a lidí
- Prostory
+ Spaces jsou nový způsob organizace místností a lidí
+ SpacesVarování, nutná podpora serveru a experimentální verze místnostiJste zváni
- Vítejte v prostorech!
+ Vítejte ve Spaces!Přidat existující místnosti a prostor
+
Jste si jisti, že chcete opustit tento prostor\?Opustit prostorPřidat místnosti
@@ -2766,26 +2767,26 @@
Pozvat lidiPozvěte do svého prostoru lidiPopis
- Zakládám prostor…
+ Zakládám Space…NahodiléObecnéJaké diskuse si přejete vést v %s\?Dejte prostoru jméno a pokračujte.
- Doplňte nějaké podrobnosti, aby prostor zaujal. Můžete je kdykoli změnit.
+ Doplňte nějaké podrobnosti, aby zaujal. Můžete je kdykoli změnit.Založit prostorPouze na pozvání, nejlepší pro Vás nebo týmyPrivátníOtevřený pro všechny, nejlepší pro komunityVeřejný
- Privátní prostor pro Vás a Vaše kolegy
+ Privátní Space pro Vás a Vaše kolegyJá a kolegové
- Privátní prostor k organizaci Vašich místností
+ Privátní Space k organizaci Vašich místnostíJen jáUjistěte se, že ti správní lidé mají přístup do %s. Můžete změnit později.S kým pracujete\?
- K vstupu do existujícího prostoru potřebujete pozvání.
+ K vstupu do existujícího Space potřebujete pozvání.Můžete změnit později
- Jaký typ prostoru chcete založit\?
+ Jaký typ Space chcete založit\?Váš privátní prostorVáš veřejný prostorPřidat prostor
@@ -2804,7 +2805,7 @@
Dovolit hostům vstoupitPozváníDoporučené místnosti
- Správa místností a prostorů
+ Správa místností a SpacesOznačit za nikoli doporučenéOznačit za doporučenéDoporučeno
@@ -2829,9 +2830,9 @@
\nK přidávání místností nemáte oprávnění.Tento prostor nemá žádné místnostiDalší informace získáte od správce domovského serveru
- Vypadá to, že váš domovský server zatím Prostory nepodporuje
+ Vypadá to, že váš domovský server zatím Spaces nepodporujeChcete experimentovat\?
-\nDo prostoru můžete přidat existující prostory.
+\nDo Spaceu můžete přidat existující Spaces.Přidat místnostiJste administrátorem tohoto prostoru, ujistěte se, že jste před odchodem převedli administrátorská práva na jiného člena.Tento prostor není veřejný. Bez pozvánky se do něj nebudete moci znovu připojit.
@@ -2842,9 +2843,9 @@
Zpětnou vazbu se nepodařilo odeslat (%s)Děkujeme, vaše zpětná vazba byla úspěšně odeslánaV případě dalších dotazů se na mě můžete obrátit
- Používáte beta verzi prostorů. Vaše zpětná vazba pomůže při tvorbě dalších verzí. Vaše platforma a uživatelské jméno budou zaznamenány, abychom mohli vaši zpětnou vazbu co nejlépe využít.
+ Používáte beta verzi Spaces. Váš feedback pomůže při tvorbě dalších verzí. Vaše platforma a uživatelské jméno budou zaznamenány, abychom mohli Váš feedback co nejlépe využít.Zpětná vazba
- Zpětná vazba prostorů
+ Feedback na SpacesOmlouváme se, při pokusu o připojení ke konferenci došlo k chyběNepojmenovaná místnostSoukromý prostor
@@ -2862,7 +2863,7 @@
V současné době se lidé nemohou připojit k soukromým místnostem, které jste vytvořili.
\n
\nV rámci beta verze to zlepšíme, ale jen jsme vás chtěli informovat.
- Prostory pro spolupracovníky nejsou ještě zcela připravené, ale přesto je můžete vyzkoušet
+ Spaces pro spolupracovníky nejsou ještě zcela připravené, ale přesto je můžete vyzkoušetOmlouváme se, došlo k chybě během pokusu o přistoupení: %sAdresa prostoruProhlédnout a spravovat adresy tohoto prostoru.
@@ -2902,17 +2903,17 @@
%d zmeškané hlasové hovory%d zmeškaných hlasových hovorů
- Rozhodněte, které prostory mají přístup do této místnosti. Pokud je vybrán prostor, jeho členové budou moci najít název místnosti a připojit se k ní.
- Další prostory nebo místnosti, které možná neznáte
+ Rozhodněte, které Spaces mají přístup do této místnosti. Pokud je vybrán prostor, jeho členové budou moci najít název místnosti a připojit se k ní.
+ Další Spaces nebo místnosti, které možná neznáteProstor, o kterém víte, že obsahuje tuto místnostRozhodněte, kdo může tuto místnost najít a připojit se k ní.
- Klepnutím upravíte prostory
- Povolit vyhledání a přístup komukoli v %s. Můžete vybrat i další prostory.
+ Klepnutím upravíte Spaces
+ Povolit vyhledání a přístup komukoli v %s. Můžete vybrat i další Spaces.Vyžadována aktualizaceKaždý, kdo se nachází v nadřazeném prostoru, bude moci tuto místnost najít a připojit se k ní - není třeba všechny zvát ručně. Tuto možnost budete moci kdykoli změnit v nastavení místnosti.Kdokoli v %s bude moci tuto místnost najít a připojit se k ní - není třeba všechny zvát ručně. Toto nastavení budete moci kdykoli změnit v nastavení místnosti.Klepnutím na nahrávku ji zastavíte nebo posloucháte
- Vybrat prostory
+ Vybrat SpacesUpozorňujeme, že aktualizací vznikne nová verze místnosti. Všechny aktuální zprávy zůstanou v této archivované místnosti.Hlasová zpráva (%1$s)Nelze odpovídat ani upravovat, když je hlasová zpráva aktivní
@@ -2929,7 +2930,7 @@
Posunutím zrušíteZahájení hlasové zprávyHlasová zpráva
- Prostory, které mají přístup
+ Spaces, které mají přístupUmožněte členům prostoru ho najít a zpřístupnit.Členové prostoru %s mohou vyhledávat, prohlížet a připojovat se.Soukromé (pouze pro pozvané)
@@ -2954,8 +2955,8 @@
Chcete-li členům prostoru pomoci najít soukromou místnost a připojit se k ní, přejděte do nastavení dané místnosti klepnutím na avatar.Pomozte členům prostoru najít soukromé místnostiDíky tomu mohou místnosti zůstat soukromé a zároveň je mohou lidé v prostoru najít a připojit se k nim. Všechny nové místnosti v prostoru budou mít tuto možnost k dispozici.
- Pomozte lidem v prostorech, aby sami našli soukromé místnosti a připojili se k nim, není třeba všechny zvát ručně.
- Novinka: Nechat lidi v prostorech najít a připojit se k soukromým místnostem
+ Pomozte lidem ve Spaces, aby sami našli soukromé místnosti a připojili se k nim, není třeba všechny zvát ručně.
+ Novinka: Nechat lidi v Spaces vyhledat a připojit se k soukromým místnostemSkupinový hovor zahájenVšechny místnosti, ve kterých se nacházíte, se zobrazí v domovském zobrazení.Zobrazit všechny místnosti v domovském zobrazení
@@ -3000,6 +3001,6 @@
Hlasový hovor s %sVideohovor s %sVyzvánění…
- Prostory
+ SpacesDozvědět se více
\ No newline at end of file
diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml
index bfa44cf467..bb61922e2a 100644
--- a/vector/src/main/res/values-de/strings.xml
+++ b/vector/src/main/res/values-de/strings.xml
@@ -2770,6 +2770,7 @@
Verlasse den Raum mit der angegebenen ID (oder den aktuellen Raum, wenn keine ID angegeben wird)Name suchenDu wurdest eingeladen
+
Bist du dir sicher, dass du den Space verlassen willst\?Space verlassenRäume hinzufügen
@@ -2823,7 +2824,7 @@
\nVersuche es später noch einmal oder kontaktiere einen Admin.Wir erstellen dir für jedes einen Raum. Du kannst jederzeit neue oder existierende Räume hinzufügen.An welchen Projekten arbeitest du gerade\?
- Dieser Raum ist öffentlich
+ Öffentlicher RaumMediendatei in Originalgröße sendenVideo in Originalauflösung senden
@@ -2854,7 +2855,7 @@
Einladen in %sPrivater SpaceÖffentlicher Space
- Dieser Space ist öffentlich
+ Öffentlicher SpaceUnbekannte PersonDieses Feature ist in der BetaFeedback geben
@@ -2928,8 +2929,8 @@
GruppenchatsVerschlüsselten DirektnachrichtenDirektnachrichten
- Nachrichten mit meinem Benutzernamen
- Nachrichten mit meinem Anzeigenamen
+ Mein Benutzername
+ Mein AnzeigenameBenachrichtige mich beiAndereErwähnungen und Schlüsselwörter
@@ -2958,4 +2959,43 @@
Sprachanruf mit %sVideoanruf mit %sMehr erfahren
+ Gruppenanruf gestartet
+ Alle beigetretenen Räume werden auf der Startseite angezeigt.
+ Alle Räume auf der Startseite anzeigen
+ Wische, um den Anruf zu beenden
+ Aktiver Anruf (%1$s) ·
+
+ Aktiver Anruf ·
+ %1$d aktive Anrufe ·
+
+ Verbindung fehlgeschlagen
+ Videoanruf abgelehnt
+ Sprachanruf abgelehnt
+ Videoanruf beendet • %1$s
+ Sprachanruf beendet • %1$s
+ Benachrichtige mich bei
+ \@room
+ Schlüsselwörter dürfen kein \"%s\" enthalten
+ Schlüsselwörter können nicht mit einem Punkt beginnen
+ Nichts
+ Nicht erreicht
+ Die angerufene Person ist beschäftigt.
+ Person beschäftigt
+ Klingeln…
+ Spaces
+ Verpasster Videoanruf
+ Verpasster Sprachanruf
+ Aktiver Videoanruf
+ Aktiver Sprachanruf
+ Eingehender Videoanruf
+ Eingehender Sprachanruf
+ Du hast diesen Anruf abgelehnt
+ Kontoeinstellungen
+ Schlüsselwörter
+ Neues Schlüsselwort hinzufügen
+ Deine Schlüsselwörter
+ Verknüpfe diese E-Mail-Adresse mit deinem Konto
+ Die Einladung zu diesem Raum wurde an %s gesendet, welche nicht mit deinem Konto verknüpft ist
+ Hilf Space-Mitgliedern private Räume zu finden
+ Auf deinem Mobilgerät wirst du keine Benachrichtigungen für Erwähnungen und Schlüsselwörter in verschlüsselten Räumen erhalten.
\ No newline at end of file
diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml
index e7ede2e900..c5e1149baa 100644
--- a/vector/src/main/res/values-eo/strings.xml
+++ b/vector/src/main/res/values-eo/strings.xml
@@ -2674,6 +2674,7 @@
Vi estas administranto de ĉi tiu aro. Certigu, ke vi transdonis administrajn rajtojn al alia ano, antaŭ ol vi vere foriros.Ĉi tiu aro ne estas publika. Vi ne povos ree aliĝi sen invito.Vi estas la sola persono ĉi tie. Se vi foriros, neniu plu povos aliĝi, inkluzive vin mem.
+
Ĉu vi certe volas foriri de la aro\?Foriri de aroAldoni ĉambrojn
diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml
index 7d7a24febe..7af1373a53 100644
--- a/vector/src/main/res/values-es/strings.xml
+++ b/vector/src/main/res/values-es/strings.xml
@@ -2619,6 +2619,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua
Espacio Experimental - Sala Restringida.Estas invitadoAñadir salas
+
Estas seguro de que quieres salir de este espacio\?Salir de este espacioAñadir salas
diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml
index 4413737c23..cd387f0867 100644
--- a/vector/src/main/res/values-et/strings.xml
+++ b/vector/src/main/res/values-et/strings.xml
@@ -267,7 +267,7 @@
Must teemaKäivitan teenuseSünkroniseerin…
- Kuulan, kas leidub sündmusi
+ Vaatan serverist uut teavetLärmakad teavitusedVaiksed teavitusedSõnumid
@@ -2679,6 +2679,7 @@
Kogukonnakeskused on uus võimalus siduda jututubasid ja inimesi.Tere tulemast kasutama kogukonnakeskuseid!Lisa olemasolevaid jututubasid ja kogukonnakeskuseid
+
Kas oled kindel, et soovid lahkuda kogukonnakeskusest\?Lahku kogukonnakeskusestLisa jututuba
diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml
index ec45a40abf..f59512f8cf 100644
--- a/vector/src/main/res/values-fa/strings.xml
+++ b/vector/src/main/res/values-fa/strings.xml
@@ -2679,6 +2679,7 @@
فضاها شیوهای جدید برای گروهبندی اتاقها و افراد است.به فضاها خوش آمدید!افزودن فضا و اتاقهای موجود
+
مطمئنید که میخواهید فضا را ترک کنید؟ترک فضاافزودن اتاق
diff --git a/vector/src/main/res/values-fr-rCA/strings.xml b/vector/src/main/res/values-fr-rCA/strings.xml
index ab6e988870..204580c842 100644
--- a/vector/src/main/res/values-fr-rCA/strings.xml
+++ b/vector/src/main/res/values-fr-rCA/strings.xml
@@ -2748,6 +2748,7 @@
Vous êtes admin de cet espace, assurez-vous d’avoir transféré les droits d’admin à un autre membre avant de partir.Cet espace n’est pas public. Vous ne pourrez pas le rejoindre sans invitation.Vous êtes la seule personne ici. Si vous partez, personne ne pourra entrer à l’avenir, même pas vous.
+
Voulez-vous vraiment quitter l’espace\?Quitter l’espaceAjouter des salons
diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml
index 57c19f0488..bb8bca76cd 100644
--- a/vector/src/main/res/values-fr/strings.xml
+++ b/vector/src/main/res/values-fr/strings.xml
@@ -2509,7 +2509,7 @@
Niveau de confiance par défautSélectionnéVidéo
- Ce salon a des brouillons non envoyés
+ a des brouillons non envoyésCertains messages n’ont pas été envoyésSupprimer l’avatarChanger l’avatar
@@ -2539,7 +2539,7 @@
RappelerCet appel est terminé%1$s a refusé cet appel
- Vous avez refusé cet appel %1$s
+ Vous avez refusé cet appel %sVous êtes actuellement dans cet appel%1$s a lancé un appelVous avez lancé un appel
@@ -2708,6 +2708,7 @@
Les espaces sont une nouvelle manière de regrouper les salons et les gens.Bienvenue dans les espaces !Ajouter des salons et espaces existants
+
Voulez-vous vraiment quitter l’espace \?Quitter l’espaceAjouter des salons
@@ -2772,7 +2773,7 @@
Gérer les salonsVous cherchez quelqu’un qui n’est pas dans %s \?%s vous invite
- Ce salon est public
+ Salon publicEnvoyer le média en taille d’origineEnvoyer la vidéo en taille originale
@@ -2803,7 +2804,7 @@
Salon sans nomEspace privéEspace public
- Cet espace est public
+ Espace publicPersonne inconnueTransférer à %1$sCe serveur fait déjà partie de la liste
@@ -2893,10 +2894,64 @@
Les conversations de groupeLes conversations privées chiffréesLes conversations privées
- Les messages contenant mon nom d’utilisateur
- Les messages contenant mon nom d’affichage
+ Mon nom d’utilisateur
+ Mon nom d’affichageMe notifier pourAutreMentions et mots-clésNotifications par défaut
+ %s dans les paramètres pour recevoir les invitations directement dans Element.
+ Lier cet e-mail à votre compte
+ Cette invitation à cette espace a été envoyée à %s qui n’est pas associé à votre compte
+ Cette invitation à ce salon a été envoyée à %s qui n’est pas associé à votre compte
+ Pour aider les membres de l’espace à trouver des salons privés, allez aux paramètres du salons en touchant l’avatar.
+ Aidez les membres de l’espace à trouver des salons privés
+ Cela permet de garder facilement un salon privé dans un espace, tout en laissant la possibilité aux gens de trouver l’espace et de le rejoindre. Tous les nouveaux salons de cet espace auront cette option disponible.
+ Aide les personnes dans les espaces à trouver et rejoindre des salons privés tout seuls, sans avoir à les inviter manuellement.
+ Nouveau : aidez les personnes dans les espaces à trouver et rejoindre des salons privés
+ L’appel de groupe a démarré
+ Tous les salons dans lesquels vous vous trouvez seront affichés sur l’Accueil.
+ Montrer tous les salons dans Accueil
+ Faire glisser pour terminer l’appel
+ %1$s Toucher pour revenir
+ Appel en cours (%1$s) ·
+ Appel vidéo en cours
+ Appel vocal en cours
+
+ Appel en cours ·
+ %1$d appels en cours ·
+
+ Connexion échouée
+ Pas de réponse
+ Appel vidéo manqué
+ Appel vocal manqué
+ Appel vidéo refusé
+ Appel vocal refusé
+ Appel vidéo terminé • %1$s
+ Appel vocal terminé • %1$s
+ Appel vidéo entrant
+ Appel vocal entrant
+ Vous avez refusé cet appel
+ Paramètres du compte
+ Vous pouvez gérer les notifications dans %1$s.
+ Veuillez noter que les notifications sur mentions et mots-clés ne sont pas disponible dans les salons chiffrés sur mobile.
+ Me notifier pour
+ Vous n’aurez aucune notifications sur mentions et mots-clés dans les salons chiffrés sur mobile.
+ Mots-clés
+ \@room
+ Les mots-clés ne peuvent pas contenir « %s »
+ Les mots-clés ne peuvent pas commencer par « . »
+ Ajouter un mot clé
+ Vos mots-clés
+ Aucun
+ Mentions et mots-clés seulement
+ Fin de l’appel…
+ Pas de réponse
+ L’utilisateur que vous avez appelé est occupé.
+ Utilisateur occupé
+ Appel audio avec %s
+ Appel vidéo avec %s
+ Appel en cours…
+ Espaces
+ En savoir plus
\ No newline at end of file
diff --git a/vector/src/main/res/values-ga/strings.xml b/vector/src/main/res/values-ga/strings.xml
index ee70c5a802..c5df827ee6 100644
--- a/vector/src/main/res/values-ga/strings.xml
+++ b/vector/src/main/res/values-ga/strings.xml
@@ -47,11 +47,11 @@
%dn
-
-
-
+ %ds
+
+ %ds
-
+ Ag sioncronú…Diúltaigh
@@ -225,4 +225,10 @@
Sheol %1$s greamán.Sheol tú íomhá.Sheol %1$s íomhá.
+ DAOINE
+ TEACHTAIREACHTAÍ
+ SEOMRAÍ
+ Cuardaigh
+ BAILL
+ Freastalaí baile:
\ No newline at end of file
diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml
index 4113f38efe..19bb84ff53 100644
--- a/vector/src/main/res/values-hu/strings.xml
+++ b/vector/src/main/res/values-hu/strings.xml
@@ -2490,6 +2490,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
Most te vagy a tér adminisztrátora, bizonyosodj meg arról, hogy kineveztél mást adminisztrátornak mielőtt elhagyod.Ez a tér nem nyilvános. Kilépés után csak újabb meghívóval lehet újra belépni.Csak te van itt. Ha kilépsz, akkor a jövőben senki nem tud majd ide belépni, beleértve téged is.
+
Biztos el akarod hagyni a teret\?Tér elhagyásaSzobák hozzáadása
diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml
index c0d421f493..81c4371ec5 100644
--- a/vector/src/main/res/values-in/strings.xml
+++ b/vector/src/main/res/values-in/strings.xml
@@ -48,7 +48,7 @@
DaftarMasukCopot akun
- URL Server Mula
+ URL HomeserverCariMulai Obrolan BaruAmbil foto atau video
@@ -89,7 +89,7 @@
Laporan bugAplikasi gagal saat terakhir digunakan. Apakah Anda ingin membuka halaman laporan kegagalan\?Gabung di Ruang
- URL Server Identity
+ URL Server IdentitasMulai Panggilan SuaraMasukBuat Akun
@@ -102,8 +102,8 @@
Sepertinya alamat email tidak benarSepertinya nomor telepon tidak benarKata sandi tidak cocok
- Serve Home ingin memastikan bahwa Anda bukan robot
- Server Home:
+ Homeserver ini ingin memastikan bahwa Anda bukan robot
+ Homeserver:Saya sudah verifikasi alamat email sayaKata sandi baru perlu dimasukkan.URL diawali dengan http[s]://
@@ -145,7 +145,7 @@
TerbesarUkuran fontSangat Kecil
- URL server Home
+ Nama serverPilih direktori ruangTerdapat perangkat tidak diketahui di ruangSaya verifikasi bahwa kuncinya sesuai
@@ -345,7 +345,7 @@ Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yan
Tunjukkan Daftar PerangkatAnda tidak akan dapat mengembalikan perubahan ini setelah Anda mengangkat pengguna ini agar memiliki kuasa yang setara dengan Anda.
\nApakah anda yakin untuk melanjutkan\?
- Melakukan banning pengguna akan mengeluarkannya dari ruangan ini dan mencegahnya untuk kembali masuk.
+ Melakukan pencekalan pengguna akan mengeluarkannya dari ruangan ini dan mencegahnya untuk kembali masuk.Apa benar Anda ingin mengundang %s ke percakapan ini?%1$s dan %2$s%1$s %2$s
@@ -426,7 +426,7 @@ Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yan
Ketik id atau alias ruangJelajahi direktori
- %d ruang
+ %d ruangan%1$s ruang ditemukan untuk %2$s
@@ -441,7 +441,7 @@ Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yan
Percakapan LangsungTinggalkan PercakapanLupakan
- Tambahkan Shortcut pada Homescreen
+ Tambahkan ke Layar UtamaPesanPengaturanVersi
@@ -486,7 +486,7 @@ Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yan
versi olmSyarat & ketentuan
- %d ruang
+ %d ruangan%1$s dalam %2$sCari sejarah
@@ -554,7 +554,7 @@ Ijinkan akses lewat halaman selanjutnya untuk menemukan pengguna ${app_name} yan
%d anggota
- %d ruang
+ %d ruanganAdmin komunitas belum menyediakan deskripsi panjang untuk komunitas ini.Anda telah dikeluarkan dari %1$s oleh %2$s
@@ -660,8 +660,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.\'%s\' bukanlah format alias yang validAnda tidak akan mendapat alamat utama untuk ruang ini.Peringatan alamat utama
- Tentukan sebagai Alamat Utama
- Jangan tentukan sebagai Alamat Utama
+ Tentukan sebagai alamat utama
+ Tidak tentukan sebagai alamat utamaSalin ID RuangSalin Alamat RuangEnkripsi diaktifkan untuk ruang ini.
@@ -682,7 +682,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Informasi perangkat pengirimNama perangkatNama
- ID Perangkat
+ ID SesiKunci perangkatVerifikasiSidik jari Ed25519
@@ -745,7 +745,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Kata Sandi:SerahkanMasuk sebagai
- Home Server
+ HomeserverServer IdentitasAntarmuka penggunaBahasa
@@ -837,7 +837,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Gagal mengambil token FCM:
\n%1$sPendaftaran Token
- Sukses mendaftarkan token FCM di HomeServer.
+ Berhasil mendaftarkan token FCM di homeserver.Gagal mendaftarkan token FCM ke homeserver:
\n%1$sLayanan Pemberitahuan
@@ -957,8 +957,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.%1$s mencekal %2$sAnda membatalkan pencekalan %1$s%1$s membatalkan pencekalan %2$s
- Anda meng-kick %1$s
- %1$s meng-kick %2$s
+ Anda mengeluarkan %1$s
+ %1$s mengeluarkan %2$sAnda menolak undangan%1$s menolak undanganAnda meninggalkan ruangan
@@ -1010,7 +1010,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Mengirim event m.room.server_aclMengirim pesanMengundang pengguna
- Meng-kick pengguna
+ Keluarkan penggunaMencekal penggunaMengubah izinMengubah nama ruangan
@@ -1032,11 +1032,11 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Batalkan pencekalan penggunaAlasan untuk mencekalCekal pengguna
- Pengguna yang di-kick akan menghilangkannya dari ruangan ini.
+ Pengguna yang dikeluarkan akan menghilangkannya dari ruangan ini.
\n
\nUntuk mencegah mereka bergabung lagi, Anda seharusnya mencekalnya.
- Alasan untuk meng-kick
- Kick user
+ Alasan untuk mengeluarkan
+ Keluarkan penggunaApakah Anda yakin ingin membatalkan undangan untuk pengguna ini\?Batalkan undanganMembatalkan abaian pengguna ini akan menampilkan semua pesan dari mereka.
@@ -1078,10 +1078,10 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Ini bukan alamat server Matrix yang validURL ini tidak dapat dijangkau, silakan periksaNomor telepon ini sudah ditentukan.
- Tetapkan surel untuk pemulihan akun. Gunakan surel atau nomor telepon nanti untuk dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.
- Tetapkan surel untuk pemulihan akun. Gunakan surel atau nomor telepon nanti untuk dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.
+ Tetapkan email untuk pemulihan akun. Gunakan email atau nomor telepon nanti untuk dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.
+ Tetapkan email untuk pemulihan akun. Gunakan email atau nomor telepon nanti untuk dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.Tetapkan nomor telepon, dan nanti dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.
- Tetapkan surel untuk pemulihan akun, dan nanti dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.
+ Tetapkan email untuk pemulihan akun, dan nanti dapat ditemukan oleh orang-orang yang mengenal Anda secara opsional.Masuk dengan single sign-onGunakan sebagai bawaan dan jangan tanya lagiSelalu tanya
@@ -1194,7 +1194,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Anda menambahkan %1$s dan menghapus %2$s sebagai alamat untuk ruangan ini.%1$s menambahkan %2$s dan menghapus %3$s sebagai alamat untuk ruangan ini.
- Anda menghapus %2$s sebagai alamat untuk ruangan ini.
+ Anda menghapus %1$s sebagai alamat untuk ruangan ini.%1$s menghapus %2$s sebagai alamat untuk ruangan ini.
@@ -1323,7 +1323,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.🎉 Semua server dilarang untuk berpartisipasi! Ruangan ini tidak lagi bisa digunakan.Tidak ada berubahan.Nomor telepon
- Tidak ada surel yang ditambahkan ke akun Anda
+ Tidak ada email yang ditambahkan ke akun AndaSurel• Server yang cocok dengan literal IP sekarang dilarang.• Server yang cocok dengan %s sekarang dilarang.
@@ -1335,4 +1335,988 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.Anda mengubah ACL server untuk ruangan ini.%s mengubah ACL server untuk ruangan ini.• Server yang cocok dengan IP literal diizinkan.
+ tutup
+ buka
+ Maaf, terjadi kesalahan
+ Homeserver Anda belum mendukung lazy load anggota ruang. Coba nanti.
+ Tingkatkan kinerja dengan hanya memuat anggota ruang pada tampilan pertama.
+ Lazy load anggota ruangan
+ Mohon masukkan nama pengguna.
+ Diam
+ Penurunan harga telah dinonaktifkan.
+ Penurunan harga telah diaktifkan.
+ Perintah \"%s\" membutuhkan parameter tambahan, atau beberapa parameter salah.
+ Abaikan
+ Permintaan Pembagian Kunci
+ Bagikan
+ Verifikasi
+ Sesi yang belum diverifikasi meminta kunci enkripsi.
+\nNama sesi: %1$s
+\nTerakhir dilihat: %2$s
+\nJika Anda tidak masuk pada sesi lain, abaikan permintaan ini.
+ Sesi baru meminta kunci enkripsi.
+\nNama sesi: %1$s
+\nTerakhir dilihat: %2$s
+\nJika Anda tidak masuk pada sesi lain, abaikan permintaan ini.
+ Untuk melanjutkan, Anda harus menerima persyaratan layanan ini.
+ Opsi ini memerlukan aplikasi pihak ketiga untuk merekam pesan.
+ Mulai kamera sistem daripada layar kamera khusus.
+ Tidak ada widget yang aktif
+ Kelola Integrasi
+ Tidak ada pengelola integrasi yang dikonfigurasi.
+ Baca Media yang dilindungi DRM
+ Gunakan mikrofon
+ Gunakan kamera
+ Blokir Semua
+ Izinkan
+ Widget ini ingin menggunakan sumber daya berikut:
+ Tinggalkan konferensi saat ini dan beralih ke yang lain\?
+ Maaf, terjadi kesalahan saat mencoba bergabung konferensi
+ Maaf, panggilan konferensi dengan Jitsi tidak didukung di perangkat lama (perangkat dengan OS Android di bawah 6.0)
+ ID Ruangan
+ ID Widget
+ Tema Anda
+ User ID Anda
+ URL avatar Anda
+ Nama tampilan Anda
+ Cabut akses untuk saya
+ Buka di browser
+ Muat ulang widget
+ Gagal memuat widget.
+\n%s
+ Menggunakannya dapat berbagi data dengan %s:
+ Menggunakannya dapat mengatur cookie dan berbagi data dengan %s:
+ Widget ini ditambahkan oleh:
+ Muat Widget
+ Widget
+ Widget aktif
+ LIHAT
+ %1$s: %2$s
+ ** Gagal mengirim - mohon buka ruangan
+ Saya
+ Undangan Baru
+ Pesan Baru
+ Ruangan
+ Peristiwa Baru
+ %1$s dan %2$s
+ %1$s di %2$s dan %3$s
+
+ %d notifikasi
+
+
+ %1$s: %2$d pesan
+
+
+ %d undangan
+
+ Server ini sudah ada di dalam daftar
+ Tidak dapat menemukan server ini atau daftar ruangannya
+ Penemuan
+ Kelola setelan penemuan Anda.
+ Masukkan nama server baru yang ingin Anda jelajahi.
+ Tambahkan server baru
+ Server Anda
+ ip yang tidak diketahui
+
+ %1$d/%2$d kunci berhasil diimpor.
+
+ Kelola Cadangan Kunci
+ Pemulihan Pesan Terenkripsi
+ Kunci berhasil diekspor
+ Harap buat frasa sandi untuk mengenkripsi kunci yang diekspor. Anda harus memasukkan frasa sandi yang sama untuk dapat mengimpor kunci.
+ Nama publik sesi dapat dilihat oleh orang yang berkomunikasi dengan Anda
+ Nama publik (terlihat oleh orang yang berkomunikasi dengan Anda)
+ Versi ruangan
+
+ %d pengguna yang dicekal
+
+ Space atau ruangan lain yang mungkin tidak Anda ketahui
+ Space yang Anda tahu berisi ruangan ini
+ Tentukan siapa yang dapat menemukan dan bergabung ruangan ini.
+ Tap untuk mengedit space
+ Pilih space
+ Tentukan space mana yang dapat mengakses ruangan ini. Jika space dipilih, anggotanya dapat menemukan dan bergabung dengan nama Ruangan.
+ Space yang dapat diakses
+ Izinkan anggota space untuk menemukan dan akses.
+ Anggota Space %s bisa menemukan, pratinjau, dan bergabung.
+ Siapa saja di space dengan ruangan ini dapat menemukan dan bergabung. Hanya admin ruang ini yang dapat menambahkannya ke space.
+ Anggota space saja
+ Siapa saja dapat menemukan ruangan ini dan bergabung
+ Publik
+ Hanya orang yang diundang yang dapat menemukan dan bergabung
+ Pribadi (Undangan Saja)
+ Pribadi
+ Setelan akses yang tidak diketahui (%s)
+ Siapa saja dapat mengetuk ruangan, anggota kemudian dapat menerima atau menolak
+ Tidak dapat mengambil visibilitas direktori ruangan saat ini (%1$s).
+ Publikasikan ruangan ini ke publik di direktori ruangan %1$s\?
+ Batalkan publikasi alamat ini
+ Publikasikan alamat ini
+ Tambahkan alamat lokal
+ Ruangan ini tidak memiliki alamat lokal
+ Tetapkan alamat untuk ruangan ini sehingga pengguna dapat menemukan ruangan ini melalui homeserver Anda (%1$s)
+ Alamat Lokal
+ Alamat baru yang dipublikasikan (mis. #alias:server)
+ Belum ada alamat lain yang dipublikasikan.
+ Belum ada alamat lain yang dipublikasikan, tambahkan satu di bawah ini.
+ Publikasikan ruangan ini ke publik di direktori ruangan %1$s\?
+ Hapus alamat \"%1$s\"\?
+ Batalkan publikasi alamat \"%1$s\"\?
+ Publikasi
+ Publikasi alamat baru secara manual
+ Alamat lain yang dipublikasikan:
+ Ini alamat utamanya
+ Alamat utama
+ Alamat yang dipublikasikan dapat digunakan oleh siapa saja di server mana pun untuk bergabung ruangan Anda. Untuk memublikasikan alamat, alamat tersebut harus disetel sebagai alamat lokal terlebih dahulu.
+ Alamat yang Dipublikasikan
+ Alamat Ruangan
+ Lihat dan kelola alamat space ini.
+ Alamat space
+ Lihat dan kelola alamat ruangan ini dan visibilitasnya di direktori ruangan.
+ Alamat ruangan
+ Izinkan tamu untuk bergabung
+ Akses ruangan
+ Perubahan siapa yang dapat membaca riwayat hanya akan berlaku untuk pesan berikutnya di ruang ini. Visibilitas sejarah yang ada tidak akan berubah.
+ Setelan akun
+ Anda dapat mengelola notifikasi di %1$s.
+ Harap dicatat bahwa sebutan & pemberitahuan keyword tidak tersedia di ruangan terenkripsi di ponsel.
+ Beritahu saya untuk
+ Putar suara rana
+ Pilih
+ Sumber media default
+ Pilih
+ Kompresi default
+ Media
+ Info tambahan: %s
+ Terjadi kesalahan saat memverifikasi nomor telepon Anda.
+ Sandi tidak cocok
+ Kelola email dan nomor telepon yang ditautkan ke akun Matrix Anda
+ Surel dan nomor telepon
+ Sandi tidak valid
+ Sandi
+ Perbarui Sandi
+ Terjadi kesalahan saat memverifikasi email Anda.
+ Aktifkan \'Izinkan integrasi\' di Setelan untuk melakukan ini.
+ Integrasi dinonaktifkan
+ Pengelola integrasi
+ Berikan izin
+ Izinkan integrasi
+ Mode data saver menerapkan filter tertentu sehingga pembaruan kehadiran dan pemberitahuan ketikan difilter.
+ ${app_name} perlu menjaga koneksi latar belakang rendah agar memiliki notifikasi yang handal.
+\nPada layar berikutnya Anda akan diminta untuk mengizinkan ${app_name} untuk selalu berjalan di latar belakang, mohon diterima.
+ Koneksi Latar Belakang
+ Ini akan menggantikan Kunci atau Frasa Anda saat ini.
+ Buat Kunci Keamanan baru atau atur Frasa Keamanan baru untuk cadangan yang ada.
+ Lindungi dari kehilangan akses ke pesan & data terenkripsi dengan mencadangkan kunci enkripsi di server Anda.
+ Siapkan di perangkat ini
+ Setel Ulang Cadangan Aman
+ Siapkan Cadangan Aman
+ Kelola
+ Cadangan Aman
+ Tambahkan tombol pada komposer pesan untuk membuka keyboard emoji
+ Tampilkan keyboard emoji
+ Tombol Enter pada keyboard akan mengirim pesan daripada menambahkan jeda baris
+ Kirim pesan dengan enter
+ Termasuk avatar dan perubahan nama tampilan.
+ Termasuk peristiwa undangan/gabung/keluar/pengeluaran/pencekalan dan avatar/tampilan perubahan nama.
+ Gunakan perintah /confetti atau kirim pesan yang berisi ❄️ atau 🎉
+ Tampilkan efek chat
+ Tampilkan peristiwa keadaan anggota ruangan
+ Gunakan pengelola integrasi untuk mengelola bot, jembatan, widget, dan paket stiker.
+\nPengelola integrasi menerima data konfigurasi, dan dapat memodifikasi widget, mengirim undangan ruang, dan mengatur tingkat daya dengan sepengetahuan Anda.
+ Integrasi
+
+ %d detik
+
+ %s
+\nSinkronisasi mungkin ditunda tergantung pada sumber daya (baterai) atau status perangkat (tidur).
+ Interval Sinkronisasi Pilihan
+ Gagal memperbarui setelan.
+ Anda tidak akan diberitahu tentang pesan masuk saat aplikasi berada di latar belakang.
+ Tidak ada sinkronisasi latar belakang
+ ${app_name} akan disinkronkan di latar belakang secara berkala pada waktu yang tepat (dapat dikonfigurasi).
+\nIni akan memengaruhi penggunaan radio dan baterai, dan ada juga pemberitahuan yang ditampilkan permanen menyatakan bahwa ${app_name} sedang mendengarkan peristiwa.
+ Anda tidak akan mendapatkan notifikasi untuk sebutan & keyword di ruang terenkripsi di ponsel.
+ Peningkatan ruangan
+ Pesan dari bot
+ Undangan ruangan
+ Keyword
+ \@room
+ Pesan grup terenkripsi
+ Pesan grup
+ Pesan terenkripsi langsung
+ Pesan langsung
+ Nama pengguna saya
+ Nama tampilan saya
+ Pesan yang berisi @room
+ Saat ruangan ditingkatkan
+ Pesan terenkripsi di obrolan grup
+ Pesan terenkripsi di chat satu-ke-satu
+ Keyword tidak boleh berisi \'%s\'
+ Keyword tidak boleh diawali dengan \'.\'
+ Tambahkan keyword baru
+ Keyword Anda
+ Beritahu saya untuk
+ Lainnya
+ Sebutan dan Keyword
+ Notifikasi Bawaan
+ Tidak Ada
+ Hanya Sebutan & Keyword
+ Mengakhiri panggilan…
+ Tidak ada jawaban
+ Pengguna yang Anda panggil sedang sibuk.
+ Pengguna sedang sibuk
+ Panggilan suara dengan %s
+ Panggilan video dengan %s
+ Panggilan berdering…
+ Spaces
+ Pelajari Lebih Lanjut
+ Kami mengirimi Anda email konfirmasi ke %s, mohon periksa email Anda dan klik tautan konfirmasi
+ Opsi penemuan akan muncul setelah Anda menambahkan email.
+ Memutuskan sambungan dari server identitas Anda akan membuat Anda tidak dapat ditemukan oleh pengguna lain dan Anda tidak akan dapat mengundang orang lain melalui email atau nomor telepon.
+ Kirim email dan nomor telepon
+ Anda telah memberikan persetujuan untuk mengirim email dan nomor telepon ke server identitas ini untuk menemukan pengguna lain dari kontak Anda.
+ Anda belum memberikan persetujuan untuk mengirim email dan nomor telepon ke server identitas ini untuk menemukan pengguna lain dari kontak Anda.
+ Kirim email dan nomor telepon
+ Untuk menemukan kontak yang ada yang Anda kenal, apakah Anda setuju untuk mengirim data kontak Anda (nomor telepon dan/atau email) ke server identitas yang dikonfigurasi (%1$s)\?
+\n
+\nUntuk privasi lebih, data yang dikirim akan di-hash sebelum dikirim.
+ Anda sedang berbagi email atau nomor telepon di server identitas %1$s. Anda harus menyambungkan kembali ke %2$s untuk berhenti membagikannya.
+ Setujui Persyaratan Layanan server identitas (%s) agar Anda dapat ditemukan melalui email atau nomor telepon.
+ Kami mengirimi Anda email konfirmasi ke %s, periksa email Anda dan klik tautan konfirmasi
+ Setel ulang sandi di %1$s
+ Email ini tidak terkait dengan akun apapun.
+ Aplikasi tidak dapat membuat akun di homeserver ini.
+\n
+\nApakah Anda ingin mendaftar menggunakan client web\?
+ Maaf, server ini tidak menerima akun baru.
+ Aplikasi tidak dapat masuk ke homeserver ini. Homeserver mendukung jenis masuk berikut: %1$s.
+\n
+\nApakah Anda ingin masuk menggunakan client web\?
+ Ada kesalahan terjadi saat memuat halaman: %1$s (%2$d)
+ Masukkan alamat server yang ingin Anda gunakan
+ Masukkan alamat Modular Element atau Server yang ingin Anda gunakan
+ Hosting premium untuk organisasi
+ Alamat
+ Alamat Element Matrix Services
+ Hapus riwayat
+ Lanjutkan dengan login masuk-tunggal
+ Masuk
+ Daftar
+ Masuk ke %1$s
+ Hubungkan ke server kustom
+ Hubungkan ke Element Matrix Services
+ Hubungkan ke %1$s
+ Lanjutkan
+ login masuk-tunggal
+ Masuk dengan %s
+ Daftar dengan %s
+ Lanjutkan dengan %s
+ Atau
+ Pengaturan kustom & lanjutan
+ Lainnya
+ Pelajari lebih lanjut
+ Hosting premium untuk organisasi
+ Bergabunglah dengan jutaan orang secara gratis di server publik terbesar
+ Sama seperti email, akun memiliki satu tempat, tetapi Anda dapat berkomunikasi dengan siapa saja
+ Pilih server
+ Mulai
+ Luaskan & sesuaikan pengalaman Anda
+ Ini adalah percakapan Anda. Miliki percakapan Anda.
+ Jaga percakapan tetap pribadi dengan enkripsi
+ Chat dengan orang-orang secara langsung atau dalam grup
+ Pesan belum terbaca
+ Anda membuatnya khusus undangan.
+ %1$s membuatnya khusus undangan.
+ Anda membuat akses ruangan khusus undangan.
+ %1$s membuat akses ruangan khusus undangan.
+ Anda membuat ruang publik untuk siapa pun yang mengetahui tautannya.
+ %1$s membuat ruang publik untuk siapa pun yang mengetahui tautannya.
+ Tekan lama pada sebuah ruangan untuk melihat lebih banyak pilihan
+ Anda tidak mengabaikan pengguna apapun
+ Ketik keyword untuk mencari reaksi.
+ Spoiler
+ Mengirim pesan sebagai spoiler
+ Anda tidak membuat perubahan
+ %1$s tidak membuat perubahan
+ Pengaturan ruangan
+ Tinggalkan ruangan ini
+ Hilangkan dari prioritas rendah
+ Tambahkan ke prioritas rendah
+ Hilangkan dari favorit
+ Tambahkan ke favorit
+ Pengaturan
+ Bisu
+ Sebutan saja
+ Semua pesan
+ Semua pesan (brisik)
+ Abaikan pengguna
+ Tidak ada koneksi jaringan sekarang
+ ${app_name} memerlukan izin untuk menyimpan kunci E2E Anda di penyimpanan.
+\n
+\nHarap izinkan akses pada pop-up berikutnya untuk dapat mengekspor kunci Anda secara manual.
+ Konten ini telah dilaporkan sebagai tidak pantas.
+\n
+\nJika Anda tidak ingin melihat konten dari pengguna ini, Anda bisa mengabaikan pengguna itu untuk menyembunyikan pesan dari pengguna.
+ Dilaporkan sebagai tidak pantas
+ Konten ini telah dilaporkan.
+\n
+\nJika Anda tidak ingin melihat konten dari pengguna ini, Anda bisa mengabaikan pengguna itu untuk menyembunyikan pesan dari pengguna.
+ Konten ini telah dilaporkan sebagai spam.
+\n
+\nJika Anda tidak ingin melihat konten dari pengguna ini, Anda bisa mengabaikan pengguna itu untuk menyembunyikan pesan dari pengguna.
+ Dilaporkan sebagai spam
+ Konten dilaporkan
+ ABAIKAN PENGGUNA
+ LAPOR
+ Alasan untuk melaporkan konten ini
+ Laporkan konten ini
+ Laporan kustom…
+ Tidak pantas
+ Spam
+ Tidak ada file di ruangan ini
+ %1$s di %2$s
+ FILE
+ Tidak ada media di ruangan ini
+ MEDIA
+ %1$d dari %2$d
+ Tidak bisa menangani perbagian data
+ Putar dan krop
+ Stiker
+ Galeri
+ Suara
+ Kamera
+ Kontak
+ Ada kesalahan terjadi saat menerima lampiran.
+ Tambahkan gambar dari
+ File
+ %d+
+ +%d
+ %1$s: %2$s
+ %1$s:
+ %1$s: %2$s %3$s
+ Tambahkan tab terdedikasi untuk notifikasi yang belum dibaca di layar utama.
+ File ini terlalu besar untuk diupload.
+ Cadangan mempunyai tanda tangan yang tidak valid dari sesi %s yang belum diverifikasi
+ Cadangan mempunyai tanda tangan yang tidak valid dari sesi %s yang terverifikasi
+ Cadangan mempunyai tanda tangan yang valid dari sesi %s yang belum diverifikasi
+ Mengirim gambar mini (%1$s / %2$s)
+ Mengunduh file %1$s…
+ File \'%1$s\' (%2$s) terlalu besar untuk diupload. Batasnya adalah %3$s.
+
+ %d pengguna telah membaca
+
+ %s telah membaca
+ %1$s, %2$s dan%3$s telah membaca
+
+ %1$s, %2$s dan %3$d lainnya telah membaca
+
+ %1$s dan %2$s telah membaca
+ Fitur ini masih dalam beta
+ Lompat ke bawah
+ Sembunyikan kata sandi
+ Tampilkan kata sandi
+ Tutup banner cadangan kunci
+ Buat ruangan baru
+ Buat pesan langsung baru dengan scan kode QR
+ Buat pesan langsung baru dengan ID Matrix
+ Buat pesan langsung baru
+ Tutup menu buat ruangan…
+ Buka menu buat ruangan
+ Buka laci navigasi
+ Kirim lampiran
+ Sepertinya server terlalu lama merespons, hal ini dapat disebabkan oleh konektivitas yang buruk atau kesalahan pada server. Silakan coba lagi dalam beberapa saat.
+ Silakan coba lagi setelah Anda menerima syarat dan ketentuan homeserver Anda.
+ Log verbose akan membantu pengembang dengan menyediakan lebih banyak log saat Anda mengirim RageShake. Bahkan ketika diaktifkan, aplikasi tidak mencatat isi pesan atau data pribadi lainnya.
+ Aktifkan log verbose.
+ Kode verifikasi salah.
+ Kode
+ Sebuah pesan teks telah dikirim ke %s. Silakan masukkan kode verifikasi yang ada di dalamnya.
+ Server identitas yang Anda pilih tidak memiliki persyaratan layanan apapun. Hanya lanjutkan jika Anda memercayai pemilik layanan
+ Server identitas tidak memiliki persyaratan layanan
+ Mohon masukkan URL server identitas
+ Tidak dapat terhubung ke server identitas
+ Masukkan URL server identitas
+ Berikan persetujuan
+ Cabut persetujuan saya
+ Tertunda
+ Nomor telepon yang dapat ditemukan
+ Opsi penemuan akan muncul setelah Anda menambahkan nomor telepon.
+ Surel yang dapat ditemukan
+ Saat ini Anda tidak menggunakan server identitas. Untuk menemukan dan dapat ditemukan oleh kontak yang Anda kenal, atur salah satu di bawah ini.
+ Saat ini Anda menggunakan %1$s untuk menemukan dan dapat ditemukan oleh kontak yang Anda kenal.
+ Ganti server identitas
+ Atur server identitas
+ Putuskan server identitas
+ Server identitas
+ Baca di
+ Gunakan bot, jembatan, widget, dan paket stiker
+ Bisa ditemukan oleh lain
+ Lihat Persyaratan
+ Persyaratan Layanan
+ Tampilkan Sejarah Suntingan
+ Menggabung ruangan…
+ Saran
+ Kontak
+ Pengguna yang Dikenal
+ Terkini
+ Saring oleh nama pengguna atau ID…
+ Mulai ketik untuk mendapatkan hasil
+ Tidak ada hasil, gunakan Tambah dengan ID Matrix untuk mencari di server.
+ Membuat ruangan…
+ Kode QR
+ Tambah dengan kode QR
+ Tambah dengan ID Matrix
+ Tautan disalin ke klipboard
+ Aktifkan geser untuk balas di linimasa
+ Cari Nama
+ Cari dengan nama atau ID
+ Nama atau ID (#contoh:matrix.org)
+ Tampilkan direktori ruangan
+ Kirim pesan langsung baru
+ Buat ruangan baru
+ Tidak bisa menemukan apa yang Anda cari\?
+ Saring obrolan…
+ Tidak ada suntingan yang ditemukan
+ Suntingan Pesan
+ (diedit)
+ File %1$s telah diunduh!
+ Mengompresi video %d%%
+ Mengompresi gambar…
+ Mengirim file (%1$s / %2$s)
+ Mengenkripsi file…
+ Tampilkan peristiwa tersembunyi di linimasa
+ Mengenkripsi gambar mini…
+ Menunggu…
+ Pesan Langsung
+ Tampilkan sejarah lengkap di ruangan terenkripsi
+ Beri Masukan
+ Masukan gagal dikirim (%s)
+ Terima kasih, saran Anda telah dikirim
+ Terima kasih, masukan Anda telah dikirim
+ Anda dapat menghubungi saya jika Anda memiliki pertanyaan lanjutan
+ Anda menggunakan versi beta space. Masukan Anda akan membantu menginformasikan versi berikutnya. Platform dan nama pengguna Anda akan dicatat untuk membantu kami menggunakan masukan Anda sebanyak yang kami bisa.
+ Masukan
+ Masukan spaces
+ Saran gagal dikirim (%s)
+ Jelaskan saran Anda di sini
+ Masukkan saran Anda di bawah.
+ Buat saran
+ Daftar token
+ Bantuan & Tentang
+ Suara & Video
+ Format:
+ Url:
+ session_name:
+ app_display_name:
+ push_key:
+ app_id:
+ Tidak ada gateway dorong terdaftar
+ Tidak ada aturan dorong ditentukan
+ Aturan Dorong
+ Ahli
+ Mengirim Anda undangan
+ Gabung ruangan untuk memulai menggunakan aplikasi.
+ Coba Lagi
+ Balas
+ Sunting
+ Sepertinya Anda mencoba menyambung ke homeserver lain. Apakah Anda ingin keluar\?
+ Tidak ada server identitas yang dikonfigurasikan, dibutuhkan untuk mengatur ulang kata sandi Anda.
+ Anda tidak menggunakan server identitas apapun
+ Kesalahan Tidak Diketahui
+ Ketidakcocokan pengguna
+ Ketidakcocokan kunci
+ Pesan yang tidak valid diterima
+ Sesi telah menerima pesan yang tidak terduga
+ SAS tidak cocok
+ Komitmen hash tidak cocok
+ Sesi tidak bisa setuju dengan persetujuan kunci, hash, MAC, atau metode SAS
+ Sesinya tidak tahu tentang transaksi itu
+ Waktu proses verifikasi habis
+ Pengguna telah membatalkan proses verifikasi
+ %s ingin memverifikasi sesi Anda
+ Permintaan Verifikasi
+ Verifikasi Sesi Interaktif
+ Proses verifikasi dibatalkan.
+\nAlasan: %s
+ Pengguna telah membatalkan proses verifikasi.
+\n%s
+ Permintaan Dibatalkan
+ Kunci Verifikasi
+ Gunakan verifikasi legacy.
+ Tidak ada yang muncul\? Belum semua client mendukung verifikasi interaktif. Gunakan verifikasi legacy.
+ Saya mengerti
+ Pesan aman dengan pengguna ini dienkripsi ujung-ke-ujung dan tidak bisa dibaca oleh pihak ketiga.
+ Anda telah berhasil memverifikasi sesi ini.
+ Terverifikasi!
+ Menunggu pengguna untuk konfirmasi…
+ Anda mempunyai permintaan verifikasi.
+ Verifikasi sesi ini dengan mengkonfirmasi angka yang ada di layar pengguna
+ Verifikasi sesi ini dengan mengkonfirmasi emoji yang ada di layar pengguna
+ Verifikasi sesi ini akan menandainya sebagai tepercaya, dan juga menandai sesi Anda sebagai terpercaya pada pengguna.
+ Verifikasi sesi ini untuk menandainya sebagai tepercaya. Mempercayai sesi pengguna membuat Anda tidak khawatir saat menggunakan pesan terenkripsi ujung-ke-ujung.
+ Permintaan Verifikasi Masuk
+ Mulai Verifikasi
+ Untuk keamanan maksimum, kami menyarankan Anda melakukan ini secara langsung atau menggunakan sarana komunikasi tepercaya lainnya.
+ Verifikasi dengan membandingkan string teks pendek.
+ Anda telah keluar karena kredensial yang tidak valid atau kedaluwarsa.
+ Gunakan Konfigurasi
+ ${app_name} mendeteksi konfigurasi server khusus untuk domain userId Anda \"%1$s\":
+\n%2$s
+ Respons penemuan homeserver tidak valid
+ Opsi Server Pelengkapan Otomatis
+ Tanda Tangan
+ Algoritma
+ Versi
+
+ Mencadangkan %d kunci…
+
+ Semua kunci tercadangkan
+ Siapkan Cadangan Aman
+ Mencadangkan kunci Anda. Ini mungkin memakan beberapa menit…
+ Kelola di Cadangan Kunci
+ Kunci pesan aman baru
+ Cadangan Kunci Baru
+ Untuk menggunakan Cadangan Kunci di sesi ini, pulihkan dengan frasa sandi atau kunci pemulihan.
+ Gunakan Cadangan Kunci
+ Jangan kehilangan pesan terenkripsi
+ Lindungi dari kehilangan akses ke pesan & data terenkripsi
+ Cadangan Aman
+ Mulai menggunakan Kunci Pemulihan
+ Jangan kehilangan pesan terenkripsi
+ Itu saya
+ Cadangan kunci pesan aman baru telah terdeteksi.
+\n
+\nJika Anda tidak menyetel metode pemulihan baru, seseorang mungkin mencoba mengakses akun Anda. Ganti kata sandi akun Anda dan segera setel metode pemulihan baru di Pengaturan.
+ Hapus kunci enkripsi yang sudah dicadangkan dari server\? Anda akan tidak dapat menggunakan kunci pemulihan untuk membaca riwayat pesan terenkripsi.
+ Hapus Cadangan
+ Memeriksa status cadangan
+ Gagal untuk menghapus cadangan (%s)
+ Menghapus cadangan…
+ Gagal untuk mendapatkan info kepercayaan untuk cadangan (%s).
+ Cadangan mempunyai tanda tangan yang valid dari sesi %s yang terverifikasi.
+ Cadangan mempunyai tanda tangan yang valid dari sesi ini.
+ Cadangan mempunyai tanda tangan dari sesi tidak dikenal dengan ID %s.
+ Kunci Anda tidak dicadangkan dari sesi ini.
+ Cadangan Kunci belum aktif di sesi ini.
+ Cadangan Kunci telah disiapkan dengan benar untuk sesi ini.
+ Hapus Cadangan
+ Pulihkan dari Cadangan
+ Sesi kripto belum diaktifkan
+ Gagal untuk mendapatkan versi pemulihan kunci (%s).
+
+ %d kunci baru telah ditambahkan ke sesi ini.
+
+
+ Dipulihkan cadangan dengan %d kunci.
+
+ Cadangan Dipulihkan %s!
+ Cadangan tidak dapat didekripsi dengan frasa sandi ini: mohon cek bahwa Anda memasukkan frasa sandi pemulihan yang benar.
+ Mohon masukkan kunci pemulihan
+ Akses Sejarah
+ Mengimpor kunci…
+ Mengunduh kunci…
+ Mengkomputasikan kunci pemulihan…
+ Memulihkan cadangan:
+ Kesalahan jaringan: mohon cek koneksi Anda dan coba lagi.
+ Cadangan tidak dapat didekripsi dengan frasa sandi ini: mohon cek bahwa Anda memasukkan frasa sandi pemulihan yang benar.
+ Kehilangan kunci pemulihan\? Anda bisa menyiapkan yang baru di pengaturan.
+ Pemulihan Pesan
+ Masukkan Kunci Pemulihan
+ Gunakan frasa sandi pemulihan Anda untuk mengakses riwayat pesan terenkripsi Anda
+ Tidak tahu frasa sandi pemulihan Anda, Anda bisa %s.
+ gunakan kunci pemulihan Anda
+ Gunakan frasa sandi pemulihan Anda untuk mengakses riwayat pesan terenkripsi Anda
+ Mengambil versi cadangan…
+ Anda mungkin kehilangan akses ke pesan Anda jika Anda keluar atau kehilangan perangkat ini.
+ Yakin\?
+ Kunci enkripsi Anda sekarang sedang dicadangkan di latar belakang ke homeserver Anda. Pencadangan awal dapat memakan waktu beberapa menit.
+ Pencadangan Dimulai
+ Kesalahan tidak terduga
+ Kunci Pemulihan
+ Membuat Kunci Pemulihan menggunakan frasa sandi, proses ini bisa memakan beberapa detik.
+ Bagikan kunci pemulihan ke…
+ Mohon membuat salinan
+ Berhenti
+ Ganti
+ Sepertinya Anda sudah menyiapkan cadangan kunci dari sesi lain. Apakah Anda ingin menggantinya dengan yang Anda buat\?
+ Cadangan sudah ada di homeserver Anda
+ Kunci pemulihan sudah disimpan.
+ Kunci pemulihan telah disimpan ke \'%s\'.
+\n
+\nPeringatan: file ini dapat dihapus jika aplikasi dihapus.
+ Simpan sebagai File
+ Bagikan
+ Simpan Kunci Pemulihan
+ Saya sudah membuat salinan
+ Selesai
+ Simpan kunci pemulihan Anda di suatu tempat yang sangat aman, seperti manajer kata sandi (atau brankas)
+ Kunci pemulihan Anda adalah jaring pengaman — Anda dapat menggunakannya untuk memulihkan akses ke pesan terenkripsi Anda jika Anda lupa frasa sandi Anda.
+\nSimpan kunci pemulihan Anda di suatu tempat yang sangat aman, seperti manajer kata sandi (atau brankas)
+ Kunci Anda sedang dicadangkan.
+ Berhasil!
+ (Lanjutan) Atur dengan Kunci Pemulihan
+ Atau, amankan cadangan Anda dengan Kunci Pemulihan, disimpan di tempat yang aman.
+ Membuat Cadangan
+ Atur Frasa Sandi
+ Kami akan menyimpan salinan kunci terenkripsi Anda di homeserver Anda. Lindungi cadangan Anda dengan frasa sandi supaya tetap aman.
+\n
+\nUntuk keamanan maksimum, ini harus berbeda dari kata sandi akun Anda.
+ Amankan cadangan Anda dengan frasa sandi.
+ Ekspor kunci secara manual
+ (Lanjutan)
+ Mulai menggunakan Cadangan Kunci
+ Pesan di ruangan terenkripsi diamankan dengan enkripsi ujung-ke-ujung. Hanya Anda dan penerima memiliki kunci untuk membaca pesan-pesan ini.
+\n
+\nCadangkan kunci Anda dengan aman untuk menghindari kehilangan kunci Anda.
+ Jangan pernah kehilangan pesan terenkripsi
+ Tidak ada sesi Matrix yang tersedia
+ Mohon hapus frasa sandinya jika Anda ingin ${app_name} untuk membuat kunci pemulihan.
+ Frasa sandi terlalu lemah
+ Mohon masukkan frasa sandi
+ Frasa sandi tidak cocok
+ Buat frasa sandi
+ Tidak menemukan APK Layanan Google Play yang valid. Notifikasi mungkin tidak berkerja dengan seharusnya.
+ Hanya untuk kesalahan
+ Untuk pesan dan kesalahan
+ Selalu
+ Keamanan & Privasi
+ Preferensi
+ Umum
+ Reaksi Cepat
+ Anda sudah menampilkan ruangan ini!
+ Pemberitahuan pihak ketiga lainnya
+ Versi SDK Matrix
+ Impor kunci E2E dari file \"%1$s\".
+ Sebuah kesalahan terjadi mendapatkan data cadangan kunci
+ Sebuah kesalahan terjadi mendapatkan info kepercyaan
+ Ruangan telah dibuat, tetapi beberapa undangan belum terkirim karena alasan berikut:
+\n
+\n%s
+ Publikasikan ruangan ini ke direktori ruangan
+ Direktori Ruangan
+ Siapa saja bisa bergabung ruangan ini
+ Publik
+ Pengaturan ruangan
+ Topik
+ Topik ruangan (opsional)
+ Nama
+ Nama ruangan
+ BUAT
+ Ruangan Baru
+ Pesan Langsung
+ Ruangan
+ Ruangan ini tidak bisa ditampilkan. Apakah Anda masih mau bergabung\?
+ Ruangan ini tidak bisa di akses di waktu ini.
+\nCoba lagi nanti, atau tanya admin ruangan untuk memeriksa jika Anda punya akses.
+ Tampilan ruang yang dapat dibaca oleh dunia belum didukung di ${app_name}
+ Tampilkan permintaan
+ Tampilkan area info
+ Ruangan ini tidak bisa di tampilkan
+ Semua Komunitas
+ Mohon menunggu…
+ Ganti jaringan
+ Ganti
+ Tidak ada jaringan. Mohon cek koneksi Internet Anda.
+ Buat Ruangan Baru
+ Peristiwa salah, tidak bisa ditampilkan
+ Terakhir disunting oleh %1$s di %2$s
+ Peristiwa dihapus oleh admin ruangan
+ Peristiwa dihapus oleh pengguna
+ Tampilkan tempat penampung untuk pesan terhapus
+ Tampilkan pesan terhapus
+ Pesan dihapus
+ Reaksi
+ Tampilkan Reaksi
+ Tambah Reaksi
+ Suka
+ Setuju
+ Reaksi
+ Ruangan Anda akan ditampilkan di sini. Ketuk tombol + untuk memulai yang baru.
+ Ruangan
+ Obrolan pesan langsung Anda akan ditampilkan di sini. Ketuk tombol + untuk memulai yang baru.
+ Obrolan
+ Lihat pesan yang belum dibaca di sini
+ Selamat datang kembali!
+ Anda tidak mempunyai pesan yang belum dibaca
+ Anda sudah melihat semua!
+ Diundang oleh %s
+ Salah satu dari yang berikut ini dapat dikompromikan:
+\n
+\n - Homeserver Anda
+\n - Homeserver pengguna yang Anda memverifikasi yang terhubung
+\n - Koneksi internet Anda, atau koneksi internet pengguna lain
+\n - Perangkat Anda, atau perangkat pengguna lain
+ Jika Anda tidak dapat memindai kode yang di atas, verifikasi dengan membandingkan pilihan emoji yang unik.
+ Tidak Terpercaya
+ Terpercaya
+ Sesi
+ Gagal mendapatkan sesi
+ Peringatan
+ Terverifikasi
+ Verifikasi
+ Gunakan sesi yang ada untuk memverifikasi yang satu ini, memberikan aksesnya ke pesan terenkripsi.
+ Selesaikan Keamanan
+ Pengguna lain mungkin tidak mempercayainya
+ Verifikasi login ini
+
+ %d sesi aktif
+
+ Verifikasi sesi ini untuk menandai sebagai terpercaya & berikan akses ke pesan terenkripsi. Jika Anda tidak masuk ke sesi ini, akun Anda mungkin dikompromikan:
+ Sesi ini dipercaya untuk pengiriman pesan yang aman karena Anda memverifikasinya:
+ Tidak ada informasi cryptographic
+ unstable
+ stable
+ Versi Default
+ Versi Ruangan 👓
+ Batas tidak diketahui.
+ Homeserver Anda menerima lampiran (file, media, dsb.) dengan ukuran hingga %s.
+ Batas unggahan file server
+ Versi server
+ Nama server
+ Keluar dari sesi ini
+ Kelola Sesi
+ Tampilkan Semua Sesi
+ Sesi Aktif
+ Admin server Anda telah menonaktifkan enkripsi ujung-ke-ujung secara default di kamar pribadi & pesan langsung.
+ Tanda Tangan Silang dinonaktifkan
+ Tanda Tangan Silang diaktifkan.
+\nKunci dipercaya.
+\nKunci privat tidak diketahui
+ Tanda Tangan Silang diaktifkan.
+\nKunci tidak dipercaya
+ Tanda Tangan Silang diaktifkan
+\nKunci Privat di perangkat.
+ Tanda Tangan Silang
+ Sesi baru Anda telah diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya.
+ Pesan dengan pengguna ini dienkripsi ujung-ke-ujung dan tidak dapat dibaca oleh pihak ketiga.
+ Bandingkan kode dengan yang ditampilkan di layar pengguna lain.
+ Bandingkan emoji yang unik, dan pastikan mereka muncul di urutan yang sama.
+ Supaya aman, lakukan secara langsung atau gunakan cara lain untuk berkomunikasi.
+ Supaya aman, verifikasi %s dengan membandingkan kode satu-kali.
+ Aktifkan enkripsi
+ Ketika diaktifkan, enkripsi tidak bisa dinonaktifkan. Pesan yang dikirim di ruangan terenkripsi tidak bisa dilihat oleh servernya, hanya anggota ruangan. Mengaktifkan enkripsi mungkin mencegah banyaknya bot dan jembatan bekerja dengan seharusnya.
+ Aktifkan enkripsi\?
+ Ketika diaktifkan, enkripsi tidak bisa dinonaktifkan.
+ Anda tidak memiliki izin untuk mengaktifkan enkripsi ujung-ke-ujung di ruangan ini.
+ Aktifkan enkripsi ujung-ke-ujung…
+ Editor pesan
+ Linimasa
+ Mengirim emote yang dicantum berwarna pelangi
+ Mengirim pesan yang dicantum berwarna pelangi
+ Ruangan lainnya
+ Ruangan baru-baru ini
+ Sesi ini tidak dapat berbagi verifikasi ini dengan sesi Anda yang lain.
+\nVerifikasi akan disimpan secara lokal dan dibagikan dalam versi aplikasi di masa depan.
+ Hapus pengabaian
+ ${app_name} menemukan sebuah masalah ketika rendering konten dengan id \'%1$s\'
+ Ke laporan dibaca
+ ${app_name} tidak mendukung peristiwa dengan tipe \'%1$s\'
+ ${app_name} tidak mendukung peristiwa dengan tipe \'%1$s\'
+ Pesan langsung
+ Bawaan di %1$s
+ Kustom (%1$d) di %2$s
+ Admin di %1$s
+ Moderator di %1$s
+ Pengguna
+ Undangan
+ Kustom
+ Moderator
+ Admin
+ Meninggalkan ruangan…
+ Tinggalkan
+ Tinggalkan Ruangan
+ Kiriman
+
+ %1$d orang
+
+ Notifikasi
+ Pengaturan
+ Pengaturan ruangan
+ Aksi Admin
+ Lebih banyak
+ Pelajari lebih lanjut
+ Keamanan
+ Pesan di ruangan ini dienkripsi ujung-ke-ujung.
+\n
+\nPesan Anda diamankan dengan kunci dan hanya Anda dan penerima memiliki kunci unik untuk mengakses mereka.
+ Pesan di ruangan ini dienkripsi ujung-ke-ujung.
+\n
+\nPesan Anda diamankan dengan kunci dan hanya Anda dan penerima memiliki kunci unik untuk mengakses mereka.
+ Pesan ini tidak terenkripsi secara ujung-ke-ujung.
+ Pesan di ruangan ini tidak terenkripsi secara ujung-ke-ujung.
+ Untuk keamanan ekstra, verifikasi %s dengan memeriksa kode satu-kali pada kedua perangkat Anda.
+\n
+\nUntuk keamanan maksimum, lakukan ini secara langsung.
+ Menunggu untuk %s…
+ Diverifikasi %s
+ Verifikasi %s
+ Gambar kode QR
+ Verifikasi dengan Emoji
+ Verifikasi dengan membandingkan emoji
+ Jika Anda tidak ada secara langsung, bandingkan emoji saja
+ Verifikasi dengan membandingkan emoji saja
+ Tidak dapat memindai
+ Pindai dengan perangkat ini
+ Pindai kodenya
+ Pindai kodenya dengan perangkat lain atau ganti dan pindai dengan perangkat ini
+ Pindai kodenya dengan perangkatnya pengguna lain untuk verifikasi sesama lain dengan aman
+ Anda
+ Verifikasi secara manual
+ Verifikasi sesi ini
+ Permintaan Verifikasi
+ Verifikasi Terkirim
+ Anda menerima
+ %s menerima
+ Anda membatalkan
+ %s membatalkan
+ Menunggu…
+ Kesimpulan Verifikasi
+ Bereaksi dengan: %s
+ Tombol Bot
+ Poll
+ Stiker
+ File
+ Audio
+ Suara
+ Gambar.
+ Video.
+ Tidak aman
+ Lihat perisai hijau untuk memastikan pengguna dipercaya. Percayakan semua pengguna di ruangan untuk memastikan ruangannya aman.
+ Untuk keamanan yang tinggi, gunakan alat komunikasi tepercaya lain atau lakukan ini secara langsung.
+ Verifikasi pengguna ini dengan mengkonfirmasi emoji unik berikut yang muncul di layar mereka, dengan urutan yang sama.
+ Tidak cocok
+ Cocok
+ Sign in yang tidak dipercaya
+ Domain email Anda tidak diperbolehkan untuk daftar di server ini
+ Membuat ruangan…
+ Beberapa karakter tidak diperbolehkan
+ Mohon memberikan alamat ruangan
+ Alamat ini sudah digunakan
+ Alamat space
+ Alamat ruangan
+ Admin homeserver Anda (%1$s) telah mengeluarkan Anda dari akun Anda %2$s (%3$s).
+ Daftar ke %1$s
+ Anda mungkin mengaktifkannya jika ruangan ini digunakan untuk berkolaborasi dengan tim internal di homeserver Anda. Ini tidak bisa diubah nanti.
+ Blokir siapa pun bukan bagian dari %s untuk bergabung dalam ruangan ini
+ Sembunyikan lanjutan
+ Tampilkan lanjutan
+ Ketika diaktifkan, enkripsi tidak bisa dinonaktifkan.
+ Aktifkan enkripsi
+ Menambahkan ¯\\_(ツ)_/¯ ke pesan teks biasa
+ ${app_name} mungkin sering crash ketika ada kesalahan yang tidak terduga
+ Gagal-cepat
+ Menunjukkan hanya hasil pertama, ketik huruf lagi…
+ Sesi lainnya
+ Sesi saat ini
+ Pengaturan
+ Goyangan terdeteksi!
+ Goyangkan ponsel Anda untuk menguji ambang deteksi
+ Ambang deteksi
+ Rageshake
+ Mode pengembang akan mengaktifkan fitur tersembunyi dan mungkin juga membuat aplikasinya kurang stabil. Untuk pengembang saja!
+ Mode pengembang
+ Pengaturan lanjutan
+ Lihat semua sesi saya
+ Sinkronisasi Awal…
+ Deskripsi terlalu pendek
+ Tautan matrix.to Anda tidak benar
+ Sesi saat ini hanya untuk pengguna %1$s dan Anda memberikan kredensial untuk pengguna %2$s. Ini tidak didukung oleh ${app_name}.
+\nMohon bersihkan data, terus masuk lagi ke akun lainnya.
+ Bersihkan data
+ Anda akan kehilangan akses ke pesan yang aman kecuali jika Anda masuk untuk memulihkan kunci enkripsi Anda.
+ Bersihkan semua data yang disimpan di perangkat ini\?
+\nMasuk lagi untuk mengakses data akun dan pesan Anda.
+ Bersihkan data
+ Bersihkan semua data
+ Peringatan: Data personal Anda (termasuk kunci enkripsi) masih disimpan di perangkat ini.
+\n
+\nBersihkanlah jika Anda selesai menggunakan perangkat ini, atau ingin masuk ke akun lain.
+ Bersihkan data personal
+ Kata sandi
+ Masuk
+ Masuk untuk memulihkan kunci enkripsi yang disimpan khusus di perangkat ini. Anda memerlukannya untuk melihat semua pesan di perangkat apapun secara aman.
+ Masuk
+ Anda telah keluar
+ Masuk lagi
+ Ini bisa disebabkan oleh berbagai alasan:
+\n
+\n• Anda telah mengubah kata sandi Anda pada sesi lain.
+\n
+\n• Anda telah menghapus sesi ini dari sesi lain.
+\n
+\n• Administrator server Anda telah menghilangkan akses Anda untuk keamanan.
+ Anda telah keluar
+ Dilihat oleh
+ Tidak dapat menemukan homeserver yang valid. Mohon cek pengenal Anda
+ Ini bukan pengenal pengguna yang valid. Format yang diharapkan: \'@pengguna:homeserver.org\'
+ Jika Anda tidak tahu kata sandi Anda, kembali untuk mengatur ulang.
+ ID Matrix
+ Jika Anda membuat akun di sebuah homeserver, gunakan ID Matrix Anda (mis. @pengguna:domain.com) dan kata sandi dibawah.
+ Masuk dengan ID Matrix
+ Masuk dengan ID Matrix
+ Alternatifnya, jika Anda sudah mempunyai akun dan Anda tahu pengenal Matrix Anda dan kata sandi Anda, gunakan metode ini:
+
+ Terlalu banyak permintaan. Anda bisa mencoba lagi dalam %1$d detik…
+
+ Homeserver ini terlalu usang untuk dihubungkan. Tanyakan admin homeservernya untuk ditingkatkan. Anda bisa lanjut, tetapi beberapa fitur tidak akan bekerja dengan seharusnya.
+ Homeserver ini terlalu usang untuk dihubungkan. Tanyakan admin homeservernya untuk ditingkatkan.
+ Homeserver usang
+ Kode yang dimasukkan tidak benar. Mohon dicek.
+ Kami telah mengirimkan email ke %1$s.
+\nMohon klik tautan yang ada untuk melanjutkan pembuatan akun.
+ Kami telah mengirimkan kode ke %1$s. Masukkan di bawah untuk memverifikasi bahwa itu Anda.
+ Mohon cek email Anda
+ Terima ketentuan untuk melanjutkan
+ Mohon melakukan tantangan captcha
+ Pilih homeserver kustom
+ Pilih Element Matrix Services
+ Pilih matrix.org
+ Akun Anda belum dibuat.
+\n
+\nBatalkan proses pendaftaran\?
+ Peringatan
+ Nama pengguna itu telah diambil
+ Lanjut
+ Kata sandi
+ Nama pengguna
+ Nama pengguna atau email
+ Nomor telepon kelihatannya tidak valid. Mohon dicek lagi
+ Nomor telepon internasional harus mulai dengan \'+\'
+ Mohon menggunakan format internasional (nomor telepon harus mulai dengan \'+\')
+ Lanjut
+ Kirim lagi
+ Masukkan kode
+ Konfirmasi nomor telepon
+ Lanjut
+ Nomor telepon (opsional)
+ Nomor telepon
+ Mohon menggunakan format internasional.
+ Atur nomor telepon untuk memperbolehkan orang yang Anda tahu untuk menemukan Anda secara opsional.
+ Atur nomor telepon
+ Lanjut
+ Email (opsional)
+ Email
+ Atur sebuah email untuk memulihkan akun Anda. Nantinya, Anda bisa mengizinkan orang yang Anda tahu untuk menemukan Anda dari email secara opsional.
+ Atur alamat email
+ Kata sandi Anda belum diubah.
+\n
+\nBatalkan proses penggantian kata sandi\?
+ Peringatan
+ Kembali untuk Masuk
+ Anda telah keluar dari semua sesi dan tidak akan menerima notifikasi. Untuk mengaktifkan notifikasi, masuk lagi di setiap perangkat.
+ Kata sandi Anda telah direset.
+ Berhasil!
+ Saya sudah memverifikasi alamat email saya
+ Ketuk tautan untuk mengkonfirmasi kata sandi baru Anda. Setelah Anda mengikuti petunjuk yang ada di tautan, klik bawahnya.
+ Email verifikasi terkirim ke %1$s.
+ Cek kotak masuk Anda
+ Email ini tidak tertaut dengan akun apapun
+ Lanjut
+ Mengubah kata sandi Anda akan mengatur ulang kunci enkripsi ujung-ke-ujung pada semua sesi Anda, yang akan membuat riwayat obrolan terenkripsi tidak dapat dibaca. Atur Cadangan Kunci atau ekspor kunci ruangan Anda dari sesi lain sebelum mengatur ulang kata sandi Anda.
+ Peringatan!
+ Kata sandi baru
+ Email
+ Lanjut
+ Email verifikasi akan dikirim ke kotak masuk Anda untuk mengkonfirmasi pengaturan kata sandi baru Anda.
\ No newline at end of file
diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml
index 8d69b4ded9..3308ee3268 100644
--- a/vector/src/main/res/values-it/strings.xml
+++ b/vector/src/main/res/values-it/strings.xml
@@ -277,7 +277,7 @@
InviaRispedisciRimuovi
- Citazione
+ CitaCondividiUltimiInoltra
@@ -930,7 +930,7 @@
Tutti i messaggi (rumoroso)Tutti i messaggi
- Solo le citazioni
+ Solo le menzioniSilenziosoAggiungi alla schermata inizialeAnteprima degli URL
@@ -1036,11 +1036,11 @@
Per continuare ad usare l\'Home Server %1$s devi leggerne e accettarne i termini di servizio.Leggi oraDisattiva l\'account
- Ciò renderà il tuo account inutilizzabile per l\'eternità. Non potrai più accederci e nessuno potrà ri-registrare lo stesso ID utente. Il tuo account uscirà da tutte le stanze in cui si trova attualmente e di dettagli del tuo account saranno rimossi dall\'Identity Server. Questa azione è irreversibile.
+ Ciò renderà il tuo account inutilizzabile per sempre. Non potrai più accedervi e nessuno potrà ri-registrare lo stesso ID utente. Il tuo account uscirà da tutte le stanze in cui si trova attualmente e i dettagli del tuo account saranno rimossi dal server d\'identità. Questa azione è irreversibile.
\n
\nDisattivare il tuo account non eliminerà i messaggi che hai inviato. Se vuoi che i tuoi messaggi siano dimenticati seleziona la casella qua sotto.
\n
-\nLa visibilità dei messaggi in Matrix è simile alle email. Questo vuol dire che se chiedi di \"dimenticare i tuoi messaggi\", quelli che hai inviato finora non verranno mai più condivisi con alcun nuovo utente, ma gli utenti che hanno già avuto accesso ai tuoi messaggi continueranno a potervi accedere.
+\nLa visibilità dei messaggi in Matrix è simile alle email. Questo vuol dire che se chiedi di \"dimenticare i tuoi messaggi\", quelli che hai inviato finora non verranno mai più condivisi con nuovi utenti, ma gli utenti che hanno già avuto accesso ai tuoi messaggi continueranno a poterli leggere.Per favore dimenticate tutti i messaggi che ho inviato fino al momento della disattivazione del mio account (Attenzione: in questo modo i nuovi utenti potrebbero vedere delle conversazioni incomplete)Per continuare, inserisci la tua password:Disattiva l\'account
@@ -1728,7 +1728,7 @@
Ignora utenteTutti i messaggi (rumoroso)Tutti i messaggi
- Solo citazioni
+ Solo menzioniSilenziosoImpostazioniEsci dalla stanza
@@ -2732,6 +2732,7 @@
Gli Spazi sono un nuovo modo per raggruppare stanze e contatti.Benvenuto negli Spazi!Aggiungi stanze e Spazi esistenti
+
Vuoi veramente uscire dallo Spazio\?Esci dallo SpazioAggiungi stanze
diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml
index 642b02f4aa..126942b645 100644
--- a/vector/src/main/res/values-lv/strings.xml
+++ b/vector/src/main/res/values-lv/strings.xml
@@ -486,7 +486,7 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē
PievienotiesPiekšskatījumsNoraidīt
- Pāriet uz pirmo neizlasīto ziņu.
+ Pāriet uz pirmo neizlasīto ziņu%s jūs uzaicināja pievienoties šai istabaiUzaicinājums tika nosūtīts uz %s, kura nav piesaistīta šim kontam.
\nJūs varat pierakstīties ar citu kontu vai arī pievienot šo epastu šim kontam.
@@ -545,7 +545,7 @@ Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespē
Nosūtīt nenosūtītās ziņasDzēst nenosūtītās ziņasFails nav atrasts
- Tev nav tiesību rakstīt ziņas šajā istabā
+ Tev nav tiesību rakstīt ziņas šajā istabā.UzticētiesNeuzticētiesIzrakstīties
@@ -1390,7 +1390,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.Nospiediet šeit, lai redzētu vecākas ziņasŠī istabas ir citas sarakstes turpinājumsSarakste turpinās šeit
- Šī istaba ir aizvietota un vairs nav aktīva
+ Šī istaba ir aizvietota un vairs nav aktīva.Pārskatīt tagadLai turpinātu izmantot %1$s bāzes serveri, jums ir jāpārskata un jāpiekrīt noteikumiem un nosacījumiem.Kluss
@@ -2049,7 +2049,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.Rādīt Emoji tastatūruTastatūras Enter poga sūtīs ziņu, tā vietā lai pievienotu jaunu rindkopuSūtīt ziņu ar Enter
- Iekļauj avatāra un vārda maiņas
+ Iekļauj avatara un parādāmā vārda izmaiņas.Rādīt konta notikumusRādīt cilvēku pievienošanos vai iziešanuIetver uzaicinājumus/pievienošanos/aiziešanu/izmešanu/noteikumu pārkāpšanas/profila bildes izmaiņas.
@@ -2214,7 +2214,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.Izveidot jaunu telpuLūdzu, mēģiniet vēlreiz, tiklīdz esat piekritis sava servera noteikumiem un nosacījumiem.Pašlaik jūs kopīgojat e-pasta adreses vai tālruņa numurus identitātes serverī %1$s. Lai pārtrauktu to koplietošanu, jums būs jāpārveido savienojums ar %2$s.
- Jūsu izvēlētajam identitātes serverim nav pakalpojumu sniegšanas noteikumu. Turpiniet tikai tad, ja uzticaties pakalpojuma īpašniekam.
+ Jūsu izvēlētajam identitātes serverim nav pakalpojumu sniegšanas noteikumu. Turpiniet tikai tad, ja uzticaties pakalpojuma īpašniekamIdentitātes serverim nav pakalpojumu sniegšanas noteikumuLūdzu, ievadiet identitātes servera urlIevadiet identitātes servera URL
diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml
index 0cec8007b5..8239b68b97 100644
--- a/vector/src/main/res/values-pt-rBR/strings.xml
+++ b/vector/src/main/res/values-pt-rBR/strings.xml
@@ -2815,6 +2815,7 @@
Você é admin deste espaço, assegure-se que você tem transferido direito de admin a um outro membro antes de sair.Este espaço não é público. Você não vai ser capaz de se rejuntar sem um convite.Você é a única pessoa aqui. Se você sair, ninguém vai ser capaz de se juntar no futuro, incluindo você.
+
Você tem certeza que você quer sair do espaço\?Sair de EspaçoAdicionar salas
diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml
index 9220273767..1569abe7f3 100644
--- a/vector/src/main/res/values-ru/strings.xml
+++ b/vector/src/main/res/values-ru/strings.xml
@@ -2725,7 +2725,7 @@
ПерезвонитьЭтот вызов закончился%1$s отменил(а) этот вызов
- Вы отклонили этот вызов %1$s
+ Вы отклонили этот вызов %sВы сейчас в этом вызовеВы начали вызов%1$s начал(а) вызов
@@ -2751,7 +2751,7 @@
%1$d приостановленных вызововНет учётных данных, неправильная учётная запись пользователя и/или пароль
- Вернуть
+ ВернутьсяВы уверены, что хотите удалить все неотправленные сообщения в этой комнате\?Удалить неотправленные сообщенияСообщения не удалось отправить
@@ -2789,7 +2789,7 @@
Уровень доверия по умолчаниюВыбраноВидео
- В этой комнате есть неотправленный черновик
+ есть неотправленный черновикНекоторые сообщения не были отправленыУдалить аватарСменить аватар
@@ -2812,7 +2812,7 @@
Покинуть текущую конференцию и перейти к другой\?Версия комнатыПоказать все команты в списке комнат, в том числе с чувствительным содержанием.
- Показать комнаты с чувствительным содержанием
+ Показать комнаты с деликатным содержаниемСписок комнатНовое значениеСменить
@@ -2868,6 +2868,7 @@
Вы являетесь администратором этого пространства, перед уходом убедитесь, что передали права администратора другому пользователю.Это пространство не является публичным. Вы не сможете присоединиться к нему без приглашения.Вы здесь единственный человек. Если вы уйдёте, никто не сможет присоединиться в будущем, включая вас.
+
Вы уверены, что хотите покинуть пространство\?Покинуть пространствоДобавить комнаты
@@ -2933,8 +2934,8 @@
Покинуть комнату с заданным id (или текущую комнату, если null)Присоединитесь к пространству с заданным idСоздать пространство
- Это пространство публичное
- Эта комната публичная
+ Публичное пространство
+ Публичная комнатаНепроверенныеНеизвестное лицоПеревести на %1$s
@@ -2972,7 +2973,7 @@
Добавить новый серверВаш серверЛюбой человек в пространстве с этой комнатой может найти её и присоединиться к ней. Только администраторы этой комнаты могут добавить её в пространство.
- Пространства
+ Только участники пространстваЛюбой желающий может найти комнату и присоединитьсяПубличныйТолько приглашенные люди могут найти и присоединиться
@@ -3030,7 +3031,7 @@
Нажмите для изменения пространствВыбрать пространстваОпределите, какие пространства могут получить доступ к этой комнате. Если пространство выбрано, его участники смогут найти и присоединиться к комнате.
- Пространства, к которым есть доступ
+ Пространства с доступомРазрешить участникам пространства находить и получать доступ.Участники пространства %s могут находить, просматривать и присоединяться.Приватная (только по приглашению)
@@ -3041,11 +3042,67 @@
Групповых сообщенияхЗашифрованных диалогахДиалогах
- Сообщениях, содержащие мое имя пользователя
- Сообщениях с моим именем
+ Мое имя пользователя
+ Мое отображаемое имяУведомлять меня оДругоеУпоминания и ключевые словаУведомления по умолчаниюЧтобы отправлять голосовые сообщения, предоставьте разрешение на Микрофон.
+ %s в настройках, чтобы получать приглашения непосредственно в Element.
+ Свяжите этот адрес электронной почты с вашей учетной записью
+ Приглашение в эту комнату было отправлено на %s, который не связан с вашей учетной записью
+ Приглашение в это пространство было отправлено на %s, который не связан с вашей учетной записью
+ Чтобы помочь участникам пространства найти и присоединиться к приватной комнате, перейдите в настройки этой комнаты, нажав на аватар.
+ Помогите участникам пространства в поиске приватных комнат
+ Это позволяет комнатам оставаться приватными для пространства, в то же время позволяя людям в пространстве находить их и присоединяться к ним. Все новые комнаты в пространстве будут иметь эту опцию.
+ Помогите людям в пространствах самим находить и присоединяться к приватным комнатам, без необходимости вручную приглашать всех.
+ Новое: Позвольте людям в пространствах находить и присоединяться к приватным комнатам
+ Начался групповой вызов
+ Все комнаты, в которых вы находитесь, будут отображаться в Начале.
+ Показать все комнаты в Начале
+ Проведите для завершения вызова
+ %1$s Нажмите, чтобы вернуться
+ Активный вызов (%1$s) ·
+
+ Активный вызов ·
+ %1$d активных вызова ·
+ %1$d активных вызовов ·
+ %1$d активных вызовов ·
+
+ Не удалось установить соединение
+ Нет ответа
+ Пропущенный выдеовызов
+ Пропущенный голосовой вызов
+ Видеовызов отклонен
+ Голосовой вызов отклонен
+ Выдеовызов завершен • %1$s
+ Голосовой вызов завершен • %1$s
+ Активный видеовызов
+ Активный голосовой вызов
+ Входящий видеовызов
+ Входящий голосовой вызов
+ Вы отклонили этот вызов
+ Настройки учетной записи
+ Вы можете управлять уведомлениями в %1$s.
+ Обратите внимание, что уведомления об упоминаниях и ключевых словах недоступны в зашифрованных комнатах на мобильных устройствах.
+ Уведомлять меня о
+ Вы не будете получать уведомления об упоминаниях и ключевых словах в зашифрованных комнатах на мобильных устройствах.
+ Ключевые слова
+ \@room
+ Ключевые слова не могут содержать \'%s\'
+ Ключевые слова не могут начинаться с \'.\'
+ Добавить новое ключевое слово
+ Ваши ключевые слова
+ Ничего
+ Только упоминания и ключевые слова
+ Завершение вызова…
+ Нет ответа
+ Пользователь, которого вы вызвали, занят.
+ Пользователь занят
+ Аудиовызов с %s
+ Видеовызов с %s
+ Вызов…
+ Пространства
+ Подробнее
\ No newline at end of file
diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml
index f8d08d2c6a..8f7144d965 100644
--- a/vector/src/main/res/values-sq/strings.xml
+++ b/vector/src/main/res/values-sq/strings.xml
@@ -2668,6 +2668,7 @@
Jeni ftuarMirë se vini te Hapësira!Shtoni dhoma ekzistuese dhe hapësira
+
Jeni i sigurt se doni të dilni nga hapësira\?Braktiseni HapësirënShtoni dhoma
diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml
index 9a560e5611..0310c056eb 100644
--- a/vector/src/main/res/values-sv/strings.xml
+++ b/vector/src/main/res/values-sv/strings.xml
@@ -2737,6 +2737,7 @@
Utrymmen är ett nytt sätt att gruppera rum och personer.Välkommen till utrymmen!Lägg till existerande rum och utrymme
+
Är du säker på att du vill lämna utrymmet\?Lämna utrymmeLägg till rum
diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml
index 524fb64cda..b62bbd8bd0 100644
--- a/vector/src/main/res/values-uk/strings.xml
+++ b/vector/src/main/res/values-uk/strings.xml
@@ -12,7 +12,7 @@
%1$s надсилає наліпку.%1$s запрошує вас%1$s приєднується
- %1$s залишає кімнату
+ %1$s виходить з кімнати%1$s відхиляє запрошення%1$s викидає %2$s%1$s розблоковує %2$s
@@ -68,15 +68,15 @@
\nІмпортування даних облікового записуВи вийшли. Причина: %1$s%1$s виходить. Причина: %2$s
- Ви залишили кімнату. Причина: %1$s
- %1$s залишає кімнату. Причина: %2$s
- %1$s залишає кімнату
- Ви залишили кімнату
- Ви залишили кімнату
+ Ви вийшли з кімнати. Причина: %1$s
+ %1$s виходить з кімнати. Причина: %2$s
+ %1$s виходить з кімнати
+ Ви вийшли з кімнати
+ Ви вийшли з кімнатиВи змінили адреси цієї кімнати.Ви змінили основну та альтернативну адреси цієї кімнати.Ви змінили альтернативні адреси для цієї кімнати.
- Ви змінили рівень доступу на %1$s.
+ Ви змінили рівень повноважень %1$s.Ви змінили серверні списки контролю доступу для цієї кімнати.Ви змінили назву кімнати на: %1$sВи змінили аватар кімнати
@@ -190,7 +190,7 @@
ГараздСкасуватиЗберегти
- Залишити
+ ВийтиНадіслатиНадіслати ще разПрибрати
@@ -425,8 +425,8 @@
Додати учасника1 учасник
- Залишити кімнату
- Ви впевнені, що бажаєте залишити кімнату\?
+ Вийти з кімнати
+ Ви впевнені, що хочете вийти з кімнати\?Ви впевнені, що бажаєте вилучити %s з цієї кімнати\?СтворитиOnline
@@ -437,7 +437,7 @@
Особисті повідомленняСЕАНСИЗапросити
- Залишити цю кімнату
+ Вийти з цієї кімнатиВилучити з цієї кімнатиЗаблокуватиРозблокувати
@@ -449,7 +449,8 @@
ID користувача, ім\'я або emailЗгадатиПоказати Список Пристроїв
- Ви не зможете скасувати цю дію, оскільки надаєте користувачу той же рівень доступу, що й у вас.\nВи впевнені?
+ Ви не зможете скасувати цю дію, оскільки надаєте користувачу той же рівень повноважень, що й у вас.
+\nВи впевнені\?Ви впевнені, що бажаєте запросити %s до цієї кімнати\?Запросити за ID
@@ -529,7 +530,7 @@
ОбранеНе терміновоОсобиста бесіда
- Залишити бесіду
+ Вийти з бесідиЗабутиПовідомлення
@@ -758,7 +759,7 @@
Введіть сервер для показу каталогу публічних кімнатІм\'я сервераВсі кімнати на сервері %s
- Всі місцеві кімнати %s
+ Усі кімнати сервера %sПошук в історії
@@ -778,7 +779,7 @@
Не вдалося створити розширення.Не вдалося надіслати запит.
- Рівень доступу має бути цілим додатним числом.
+ Рівень повноважень має бути цілим додатним числом.Ви не перебуваєте в цій кімнаті.Ви не маєте дозволу виконувати цю дію у цій кімнаті.У запиті відсутній room_id.
@@ -989,11 +990,11 @@
Показує діюЗаблокувати користувача із вказаним IDРозблокувати користувача із вказаним ID
- Визначити рівень доступу користувача
+ Визначити рівень повноважень користувачаСкинути рівень доступу користувача із вказаним IDЗапросити користувача із вказаним ID до поточної кімнатиПриєднатися до кімнати із вказаним псевдонімом
- Покинути кімнату
+ Вийти з кімнатиВстановити тему кімнатиКопнути користувача із вказаним IDЗмінити Ваш псевдонім
@@ -1217,7 +1218,7 @@
Вимкнути мікрофонВідерити бесідуРоль
- Призначити роль
+ Визначити рольНадіслатиНомери телефонівЕлектронні адреси
@@ -1517,7 +1518,7 @@
Показати стан подій учасників кімнатиКерування криптографічними ключамиВикористовуйте менеджер інтеграції для керування ботами, мостами, розширеннями та пакунками наліпок.
-\nМенеджери інтеграції отримують дані конфігурації та можуть змінювати розширення, надсилати запрошення до кімнати та надавати права від вашого імені.
+\nМенеджери інтеграції отримують дані конфігурації та можуть змінювати розширення, надсилати запрошення до кімнати та надавати рівень повноважень від вашого імені.Інтеграції%d секунда
@@ -1540,7 +1541,7 @@
Режим фонової синхронізаціїЗастосунки не потребують з\'єднання з homeserver у фоновому режимі, це має скоротити споживання батареїНехтувати оптимізацією
- Якщо користувач залишає пристрій від\'єднаним та нерухомим впродовж певного часу з вимкненим екраном, пристрій переходить у режим дрімання. Це запобігає доступу програм до мережі та відкладає їх завдання, синхронізацію та стандартні сигнали тривоги.
+ Якщо користувач залишає пристрій від\'єднаним та нерухомим впродовж певного часу з вимкненим екраном, пристрій переходить у режим сну. Це запобігає доступу застосунків до мережі та відкладає їхні завдання, синхронізацію та стандартні попередження.Оптимізація акумулятора не впливає на ${app_name}.Оптимізація акумулятораВимкнути обмеження
@@ -1766,10 +1767,10 @@
Виберіть ролі, необхідні для зміни різних частин кімнатиДозволиПерегляд та оновлення ролей, необхідних для зміни різних частин кімнати.
- Залишити кімнату
- Залишити кімнату
- Залишити
- Залишити поточну конференцію та перейти до іншої\?
+ Вийти з кімнати
+ Вийти з кімнати
+ Вийти
+ Вийти з поточної конференції та перейти до іншої\?Це не загальнодоступна кімната. Ви не зможете знову приєднатися без запрошення.У вас немає дозволу на ввімкнення шифрування в цій кімнаті.Дозволи кімнати
@@ -2069,7 +2070,7 @@
Не типовий (%1$d) у %2$sНе типовий%1$s з %2$s до %3$s
- %1$s змінює рівень доступу %2$s.
+ %1$s змінює рівень повноважень %2$s.Ви змінили відеоконференцію%1$s змінює відеоконференціюВи завершили відеоконфернцію
@@ -2080,7 +2081,7 @@
ЗапрошенняМодераториАдміністратори
- Залишення кімнати…
+ Вихід з кімнати…Типово у %1$sМодератор у %1$sАдміністратор у %1$s
@@ -2201,8 +2202,8 @@
Проведіть пальцем, щоб скасуватиРозпочати голосове повідомленняРозпочато груповий виклик
- Ви впевнені, що хочете залишити простір\?
- Залишити простір
+ Ви впевнені, що хочете вийти з простору\?
+ Вийти з просторуКерувати кімнатами%s запрошує васВас запрошено
@@ -2440,7 +2441,7 @@
Обернути й обрізатиФайл «%1$s» (%2$s) завеликий для вивантаження. Обмеження %3$s.Цей файл завеликий для вивантаження.
- Це бета-можливість
+ Це бета-функціяСховати парольПоказати парольСтворити нову кімнату
@@ -2482,7 +2483,7 @@
Сеанс отримав несподіване повідомленняSAS не збігаєтьсяКористувач скасовує перевірку
- %s хоче звірити сеанс
+ %s хоче звірити ваш сеансЗапит перевіркиЗапит перевіркиІнтерактивна перевірка сеансу
@@ -2509,4 +2510,72 @@
Сканувати їхній кодСкануйте код за допомогою іншого пристрою або перемкніться та скануйте за допомогою цього пристроюЗіскануйте код за допомогою пристрою іншого користувача, щоб надійно перевірити одне одного
+ Очікування на %s…
+ Ви прийняли
+ %s прийнято
+ Ви скасували
+ %s скасовано
+ Перевірка ключа
+ Читати у
+ Нічого не знайдено, натисніть Додати за matrix-ID для пошуку на сервері.
+ Пошук за назвою
+ Ви користуєтеся бета-версією просторів. Ваш відгук допоможе поліпшити наступні версії. Ваша платформа та ім’я користувача будуть зазначені, щоб допомогти нам використати ваш відгук якнайефективніше.
+ Зареєструвати токен
+ Немає зареєстрованих push-шлюзів
+ Правила push-сповіщень не визначені
+ Правила push-сповіщень
+ Ви вже переглядаєте цю кімнату!
+ Інші примітки про треті сторони
+ Імпорт ключів e2e з файлу «%1$s».
+ Сталася помилка отримання ключів резервних копій даних
+ Під час отримання інформації про довіру сталася помилка
+ Попередній перегляд цієї кімнати недоступний. Бажаєте приєднатися до неї\?
+ Зараз ця кімната недоступна.
+\nПовторіть спробу пізніше або попросіть адміністратора кімнати перевірити, чи маєте ви доступ.
+ Попередній перегляд цієї кімнати недоступний
+ Немає мережі. Перевірте інтернет-з\'єднання.
+ Хибна подія, неможливо показати
+ Перегляд реакцій
+ Тут буде показано ваші кімнати. Натисніть + унизу праворуч, щоб знайти наявні або створити власні.
+ Приєднайтеся до кімнати, щоб почати користуватися застосунком.
+ Схоже, ви намагаєтесь під\'єднатися до іншого домашнього сервера. Бажаєте вийти\?
+ Користувачі відрізняються
+ Ключі відрізняються
+ Отриманий хеш не збігається
+ Сеанс не може узгодити ключі, хеш, MAC або метод SAS
+ Сеанс не розпізнає про цю транзакцію
+ Час перевірки минув
+ Перевірте, порівнявши короткий текстовий рядок.
+ Ви вийшли з облікового запису через хибні або застарілі облікові дані.
+ Використати конфігурацію
+ ${app_name} виявив користувацьку конфігурацію сервера для вашого userID домену «%1$s»:
+\n%2$s
+ Параметри сервера автозаповнення
+
+ Резервне копіювання %d ключа…
+ Резервне копіювання %d ключів…
+ Резервне копіювання %d ключів…
+ Резервне копіювання %d ключів…
+
+ Виявлено нову резервну копію ключа зашифрованих повідомлень.
+\n
+\nЯкщо ви не налаштовували новий метод відновлення, можливо, зловмисник намагається отримати доступ до вашого облікового запису. Змініть пароль свого облікового запису та негайно визначте новий спосіб відновлення в Налаштуваннях.
+ Додати наявну кімнату до простору
+ Створити простір
+ Лише я
+ Створити простір
+ Вміст події
+ Вміст події
+ Немає вмісту
+ Тип
+ Редагувати вміст
+ Попередження про рівень довіри
+ Сталася помилка пошуку номера телефона
+ Не вдалося встановити з\'єднання
+ Ви відхилили цей виклик
+ Посилання Matrix
+ Ваша книга контактів порожня
+ Шукати у моїх контактах
+ Телефонна книга
+ Не вдалося розшифрувати
\ No newline at end of file
diff --git a/vector/src/main/res/values-vi/strings.xml b/vector/src/main/res/values-vi/strings.xml
index 0e83a210b3..5da3dbe059 100644
--- a/vector/src/main/res/values-vi/strings.xml
+++ b/vector/src/main/res/values-vi/strings.xml
@@ -15,7 +15,7 @@
%d người dùngKhông có phòng
- Thư mục phòng
+ Danh sách phòngPhòngKhông còn kết quả nào nữaLọc danh sách yêu thích
@@ -25,7 +25,7 @@
Bạn chưa cho phép ${app_name} truy cập danh bạ của bạnKhông có cuộc trò chuyện nàoChỉ những liên hệ Matrix
- Thư mục người dùng
+ Danh sách người dùngSổ địa chỉ địa phươngCuộc trò chuyệnCảnh báo hệ thống
@@ -483,7 +483,7 @@
Địa chỉ email được liên kết đến tài khoản của bạn phải được nhập.Để đặt lại mật khẩu, hãy nhập địa chỉ email được liên kết đến tài khoản của bạn:Tên người dùng đã được sử dụng
- Máy chủ nhà này muốn chắc chắn là bạn không phải rô bốt
+ Máy chủ nhà này muốn chắc chắn bạn không phải rô bốtViệc đăng ký bằng cả email và số điện thoại ngay một lúc chưa được hỗ trợ cho đến khi api có tồn tại. Chỉ có số điện thoại sẽ được tính đến.
\n
\nBạn có thể thêm email vào hồ sơ của bạn trong cài đặt.
@@ -670,7 +670,7 @@
Xác minh địa chỉ email thất bại: hãy chắc chắn là bạn đã nhấn vào liên kết trong emailHiện tất cả phòng trong thư mục phòng, bao gồm cả các phòng có nội dung phản cảm.Hiện các phòng có nội dung phản cảm
- Thư mục phòng
+ Danh sách phòngMặc định hệ thốngBạn đã bật mã hoá đầu cuối (thuật toán không được nhận ra %1$s).%1$s đã bật mã hoá đầu cuối (thuật toán không được nhận ra %2$s).
@@ -1006,4 +1006,542 @@
Người dùng bạn vừa gọi đang bận.Người dụng bậnPhòng chat của bạn được hiển thị ở đây. Bạn có thể tạo mới phòng chat hoặc tham gia các phòng cộng đồng hiện có.
+ Chọn một máy chủ
+ Hãy bắt đầu
+ Mở rộng và hiệu chỉnh trải nghiệm của bạn
+ Giữ cho hội thoải riêng tư với bảo mật đầu cuối
+ Chat với một người hoặc chat nhóm
+ Đây là hội thoại của bạn. Bạn sở hữu nó.
+ Tin nhắn chưa đọc
+ Bạn thiết lập phòng chỉ tham gia khi được mời.
+ %1$s thiết lập phòng chỉ tham gia khi được mời.
+ Bạn thiết lập phòng chỉ tham gia khi được mời.
+ %1$s thiết lập phòng chỉ tham gia khi được mời.
+ Bạn thiết lập phòng công khai cho bất kỳ ai biết đường dẫn.
+ %1$s thiết lập phòng công khai cho ai biết đường dẫn.
+ Chạm lâu vào một phòng để thấy nhiều lựa chọn
+ Bạn đang không lơ ai cả
+ Nhập từ khóa để tìm phản ứng.
+ Phá đám
+ Gửi tin phá đám
+ Bạn không tạo thay đổi
+ %1$s không tạo thay đổi
+ Thiết lập phòng
+ Rời phòng
+ Loại khỏi mục ưu tiên thấp
+ Thêm vào mục ưu tiên thấp
+ Loại khỏi mục yêu thích
+ Thêm vào mục yêu thích
+ Thiết lập
+ Im lặng
+ Chỉ tin nhắn được đề cập
+ Tất cả tin nhắn
+ Tất cả tin nhắn (ầm ĩ)
+ Lơ người dùng
+ Hiện không có kết nối mạng
+ ${app_name} cần được cấp quyền để lưu khóa bảo mật E2E keys trên bộ nhớ.
+\n
+\nVui lòng cấp quyền để App có thể xuất khẩu khóa bảo mật.
+ Nội dung này bị báo cáo không phù hợp.
+\n
+\nNếu bạn không muốn thấy thêm nội dung từ người dùng này, bạn có thể lơ họ để ẩn nội dung tin nhắn.
+ Bị báo cáo không phù hợp
+ Nội dung này bị báo cáo tin rác.
+\n
+\nNếu bạn không muốn thấy thêm nội dung từ người dùng này, bạn có thể lơ họ để ẩn nội dung tin nhắn.
+ Báo cáo tin rác
+ Nội dung này bị báo cáo.
+\n
+\nNếu bạn không muốn thấy thêm nội dung từ người dùng này, bạn có thể lơ họ để ẩn nội dung tin nhắn.
+ Nội dung bị báo cáo
+ LƠ NGƯỜI DÙNG
+ BÁO CÁO
+ Lí do báo cáo nội dung này
+ Báo cáo nội dung này
+ Báo cáo tùy chọn…
+ Tin nhắn không phù hợp
+ Tin nhắc rác
+ Không có tệp nào trong phòng này
+ %1$s vào lúc %2$s
+ Tệp
+ Không có media nào trong phòng này
+ MEDIA
+ %1$d của %2$d
+ Không thể xử lý dữ liệu chia sẻ
+ Xoay chiều và cắt bớt
+ Sticker
+ Bộ sưu tập
+ Âm thành
+ Máy ảnh
+ Liên hệ
+ Tệp
+ Thêm hình từ
+ Lỗi xảy ra khi hiển thị tệp đính kèm.
+ Tệp \'%1$s\' (%2$s) quá lớn để tải lên. Dung lượng tối đa là %3$s.
+ Tệp quá lớn để tải lên.
+
+ %d người dùng đã đọc
+
+ %s đã đọc
+ %1$s và %2$s đã đọc
+ %1$s, %2$s và %3$s đã đọc
+
+ %1$s, %2$s và %3$d người khác đã đọc
+
+ Chức năng thử nghiệm
+ Nhảy xuống đáy
+ Ẩn mật khẩu
+ Hiện mật khẩu
+ Đóng banner sao lưu khóa
+ Tạo phòng chat mới
+ Tạo phòng chat 1-1 bằng cách quét mã QR
+ Tạo mới phòng chat 1-1 theo ID Matrix
+ Tạo mới phòng chat 1-1
+ Đóng menu tạo phòng…
+ Mở menu tạo phòng
+ Gửi tệp đính kèm
+ Bạn hiện đang sử dụng %1$s để khám phá và được khám phá bởi danh bạ hiện hữu.
+ Đổi máy chủ định danh
+ Cấu hình máy chủ định danh
+ Ngắt kết nối máy chủ định danh
+ Máy chủ định danh
+ Đọc lúc
+ Sử dụng Bot, cầu nối, widget hoặc sticker
+ Được khám phá bởi người khác
+ Xem lại Điều khoản
+ Điều khoản Dịch vụ
+ Xem lịch sử chỉnh sửa
+ Tham gia phòng…
+ Gợi ý
+ Danh bạ
+ Thành viên đã biết
+ Gần đây
+ Lọc theo tên đăng nhập hoặc ID…
+ Gõ phím để tìm kết quả
+ Mã QR
+ Thêm vào phòng bằng QR code
+ Thêm vào phòng bằng ID
+ Đường link được copy
+ Bật chức năng quẹt để Trả lời
+ Tìm Tên
+ Tìm theo tên hoặc ID
+ Tên hoặc ID
+ Xem danh mục phòng chat
+ Gửi tin nhắn tới phòng 1-1
+ Tạo phòng chat mới
+ Không tìm thấy kết quả cần tìm\?
+ Không tìm thấy, sử dụng chức năng thêm thành viên bằng username.
+ Tạo phòng chat…
+ Chào mừng về nhà!
+ Tất cả tin nhắn đã được đọc
+ Bạn đã thấy tất cả tin nhắn!
+ Được mời bởi %s
+ Lời mời đã được gửi
+ Tham gia phòng chat để bắt đầu sử dụng app.
+ Thử lại
+ Phản hồi
+ Sửa
+ Hình phòng
+ Mãi mãi
+ 1 tháng
+ 1 tuần
+ 3 ngày
+ Bạn hiện không là thành viên của bất cứ cộng đồng nào.
+ Biểu tượng
+ Phát âm thanh shutter
+ Chọn
+ Nguồn media mặc định
+ Chọn
+ Nén dữ liệu mặc định
+ Media
+ Thông tin bổ sung: %s
+ Lỗi xảy ra khi xác thực số điện thoại của bạn.
+ Mã
+ Lỗi khi xác thực số điện thoại
+ Nhập mã kích hoạt
+ Chúng tôi vừa gửi tin nhắn có mã kích hoạt. Vui lòng nhập mã này bên dưới.
+ Xác thực số điện thoại
+ Số điện thoại không hợp lệ với quốc gia này
+ Số điện thoại
+ Vui lòng chọn quốc gia
+ Quốc gia
+ Chọn quốc gia
+ Bạn có chắc muốn hủy %1$s %2$s\?
+ Bạn có chắc muốn hủy bỏ mục tiêu thông báo này\?
+ Quản lý email và số điện thoại liên kết với tài khoản Matrix
+ Email và số điện thoại
+ Mật khẩu không khớp
+ Hiện tất cả tin nhắn từ %s\?
+\n
+\nLưu ý rằng hành động này sẽ khởi động App và có thể mất nhiều thời gian.
+ Mật khẩu của bạn vừa được cập nhật
+ Mật khẩu này không hợp lệ
+ Cập nhật mật khẩu thất bại
+ Cập nhật mật khẩu
+ Xác nhận mật khẩu mới
+ Mật khẩu mới
+ Mật khẩu hiện tại
+ Đổi mật khẩu
+ Mật khẩu
+ Lỗi xảy ra khi xác thực địa chỉ email của bạn.
+ Số điện thoại này đã được sử dụng.
+ Địa chỉ email này không tìm thấy.
+ Địa chỉ Email này đã được sử dụng.
+ Chờ xác thực
+ Chọn ngôn ngữ
+ Ngôn ngữ
+ Giao diện người dùng
+ Bật \'Cho phép Tích hợp\' trong mục Thiết lập để thực thi tác vụ này.
+ Tích hợp đang bị tắt
+ Quản lý tích hợp
+ Cho phép tích hợp
+ Máy chủ định danh
+ Máy chủ
+ Đăng nhập tài khoản
+ Gửi
+ Mật khẩu:
+ Đăng nhập
+ Tác vụ này cần đăng nhập lại.
+\nNhập mật khẩu để tiếp tục.
+ %1$s @ %2$s
+ Thấy lần cuối
+ Cập nhật Tên công khai
+ Tên công khai
+ ID
+ Thông tin phiên
+ Chế độ tiết kiệm dữ liệu
+ Có, Tôi muốn giúp!
+ Cho phép lấy dữ liệu phân tích để giúp cải thiện ${app_name}.
+ ${app_name} thu thập dữ liệu khuyết danh để giúp chúng tôi cải thiện app.
+ Gửi dữ liệu phân tích
+ Phân tích
+ Cấp quyền
+ Điều này sẽ thay thế Chìa khóa hoặc Chuỗi ký tự hiện tại.
+ Tạo Chìa khóa An toàn mới hoặc đặt Chuỗi từ An toàn để bảo vệ sao lưu hiện hữu.
+ Sao lưu các chìa khóa giải mã trên server của bạn để đảm bảo không mất dữ liệu và tin nhắn mã hóa.
+ Thiết lập trên thiết bị này
+ Đặt lại sao lưu an toàn
+ Thiết lập sao lưu an toàn
+ Quản lý
+ Sao lưu an toàn
+ Thêm nút trên dãy phím soạn tin để bật bàn phím emoji
+ Hiện bàn phím emoji
+ Nhấn Enter ở bàn phím ảo sẽ gửi tin thay vì thêm cách dòng mới
+ Gửi tin bằng phím Enter
+ Xem trước media trước khi gửi
+ Rung khi đề cập tên người dùng
+ Bao gồm thay đổi tên hiển thị và hình đại diện.
+ Hiện các sự kiện của tài khoản
+ Sự kiện mời, loại hoặc cấm thành viên không bị ảnh hưởng.
+ Hiện sự kiện tham gia hoặc rời phòng
+ Bao gồm sự kiện mời/tham gia/rời/loại thành viên/cấm thành viên và thay đổi tên/avatar.
+ Gõ lệnh /confetti hoặc gửi tin chứa ❄️ hoặc 🎉
+ Hiện hiệu ứng chat
+ Hiện trạng thái hoạt động của thành viên phòng
+ Chạm vào thông báo tin đã đọc để xem chi tiết.
+ Hiện thông báo tin đã đọc
+ Hiện dấu thời gian theo chuẩn 12-giờ
+ Hiện Dấu thời gian cho tất cả tin nhắn
+ Định dạng tin nhắn theo chuẩn markdown trước khi gửi.
+ Định dạng markdown
+ Cho người dùng khác biết bạn đã gõ phím.
+ Gửi thông báo đang gõ tin nhắn
+ Xem trước trang web trong phòng chat nếu máy chủ bật hỗ trợ chức năng này.
+ Xem trước URL
+ Phiên làm việc
+ Ghim phòng có tin nhắn chưa đọc
+ Ghim phòng có thông báo nhỡ
+ Màn hình home
+ Danh bạ quốc gia
+ Quyền danh bạ
+ Danh bạ trên máy
+ Mục tiêu Thông báo
+ Quản lý Chìa khóa mã hóa bảo mật
+ Mã hóa bảo mật
+ Tích hợp
+ Nâng cao
+ Khác
+ Người dùng bị bỏ qua
+ Thông báo
+ Thiết lập người dùng
+ Xóa media cache
+ Xóa cache
+ Giữ media
+ Điều khoản riêng tư
+ Bản quyền
+ Lưu ý bên thứ 3
+ Điều khoản Dịch vụ
+ Phiên bản olm
+ Phiên bản
+
+ %d giây
+
+ Thời gian chờ giữa 2 lần đồng bộ
+ %s
+\nTiến trình đồng bộ có thể bị gián đoạn tùy thuộc vào lượng pin và trạng thái hoạt động của thiết bị.
+ Mã nhập vào không hợp lệ. Vui lòng kiểm tra.
+ Chúng tôi vừa gửi email tới %1$s.
+\nClick vào đường link trong email để tiếp tục quá trình tạo tài khoản.
+ Hãy kiểm tra email của bạn
+ Chấp nhận điều khoản để tiếp tục
+ Hãy thực hiện thách thức captcha
+ Chọn một máy chủ khác
+ Chọn Dịch vụ Element Matrix
+ Chọn matrix.org
+ Tài khoản của bạn chưa được tạo.
+\n
+\nBạn muốn ngừng tiến trình đăng ký\?
+ Cảnh báo
+ Tên đăng nhập này đã được đăng ký
+ Tiếp
+ Mật khẩu
+ Tên đăng nhập
+ Username hoặc email
+ Đăng ký với %1$s
+ Số điện thoại có vẻ không hợp lệ. Hãy kiểm tra lại
+ Số điện thoại quốc tế phải bắt đầu với dấu \'+\'
+ Vui lòng dùng định dạng quốc tế (số điện thoại bắt đầu với \'+\')
+ Tiếp
+ Gửi lại
+ Nhập mã
+ Chúng tôi vừa gửi mã tới %1$s. Nhập mã để xác thực.
+ Xác nhận số điện thoại
+ Tiếp
+ Số điện thoại (tùy chọn)
+ Số điện thoại
+ Vui lòng sử dụng mẫu quốc tế.
+ Thêm số điện thoại để tùy chọn cho phép người khác tìm bạn qua số điện thoại.
+ Thêm số điện thoại
+ Tiếp
+ Email (tùy chọn)
+ Email
+ Thêm địa chỉ email để phục hồi tài khoản. Sau này bạn có thể tùy chọn cho phép người khác tìm mình qua email.
+ Thêm địa chỉ email
+ Mật khẩu chưa được thay đổi.
+\n
+\nBạn muốn ngừng tiến trình đổi mật khẩu\?
+ Cảnh báo
+ Trở lại Đăng nhập
+ Bạn vừa đăng xuất tất cả phiên đăng nhập và không còn nhận được thông báo đẩy. Đăng nhập lại để nhận thông báo trên thiết bị.
+ Mật khẩu của bạn đã được đặt lại.
+ Thành công!
+ Tôi đã xác minh địa chỉ email
+ Nhấp vào đường dẫn để xác nhận mật khẩu mới. Sau khi bạn nhâp vào đường dẫn, hãy nhấp vào bên dưới.
+ Email xác thực đã được gửi tới %1$s.
+ Kiểm tra mailbox
+ Email này không gắn với tài khoản nào
+ Tiếp tục
+ Đổi mật khẩu sẽ đặt lại tất cả khóa bảo mật trên tất cả phiên của bạn, làm cho lịch sử chat mã hóa không đọc được. Vui lòng Sao lưu Khóa hoặc xuất khẩu tất cả khóa bảo mật các phòng từ một phiên đăng nhập khác trước khi đặt lại mật khẩu.
+ Cảnh báo!
+ Mật khẩu mới
+ Email
+ Tiếp
+ Email xác thực thông tin đã được gửi tới bạn để xác nhận đặt lại mật khẩu mới.
+ Đặt lại mật khẩu ở %1$s
+ Địa chỉ email này không có trong hệ thống.
+ Địa chỉ
+ Địa chỉ Dịch vụ Element Matrix
+ Xóa lịch sử
+ Tiếp tục với SSO
+ Đăng Nhập
+ Đăng Ký
+ Đăng nhập vào %1$s
+ Kết nối tới máy chủ dịch vụ khác
+ Kết nối tới máy chủ dịch vụ
+ Kết nố tới %1$s
+ Tiếp tục
+ Single Sign-On
+ Đăng nhập với %s
+ Tạo tài khoản với %s
+ Tiếp tục với %s
+ Hoặc
+ Thiết lập tùy chỉnh và nâng cao
+ Khác
+ Xem thêm
+ Giống như email, tài khoản cần có nhà riêng, dù bạn có thể nói chuyện với bất kỳ ai
+ Thiết lập tùy chỉnh.
+ Khả dụng
+ Thông báo không được bật cho phiên này.
+\nVui lòng kiểm tra trong thiết lập ứng dụng ${app_name}.
+ Thông báo được bật cho phiên này.
+ Thiết lập phiên.
+ Khả dụng
+ Thông báo bị tắt cho tài khoản của bạn.
+\nVui lòng kiểm tra thiết lập.
+ Thông báo được bật cho tài khoản của bạn.
+ Thiết lập tài khoản.
+ Mở thiết lập
+ Thông báo bị tắt trong thiết lập hệ thống.
+\nVui lòng kiểm tra thiết lập.
+ Thông báo được bật trong thiết lập hệ thống.
+ Thiết lập hệ thống.
+ Xử lý lỗi thông báo
+ Chính sách riêng tư của thông báo
+ Từ khóa không được chứa \'%s\'
+ Từ khóa không được bắt đầu với \'.\'
+ Thêm mới từ khóa
+ Từ khóa của bạn
+ Thông báo tôi về
+ Khác
+ Đề cập tới và từ khóa
+ Thông báo mặc định
+ Mức quan trọng của thông báo theo sự kiện
+ Thiết lập Thông báo nâng cao
+ Đảm bảo rằng bạn nhấp vào đường link trong email được gửi tới bạn.
+ Loại bỏ %s\?
+ Số điện thoại
+ Không có địa chỉ email nào trong tài khoản của bạn
+ Địa chỉ email
+ Yêu cầu đăng nhập
+ Bạn không thể làm việc này từ ứng dụng ${app_name}
+ Xác nhận mật khẩu
+ Hiển thị thông tin ứng dụng trong thiết lập hệ thống.
+ Thông tin ứng dụng
+ Thêm số điện thoại
+ Chưa có số điện thoại trong tài khoản của bạn
+ Điện thoại
+ Thêm địa chỉ email
+ Email
+ Tên hiển thị
+ Hình đại diện
+ Chính sách riêng tư
+ Bản quyền
+ Lưu ý bên thứ ba
+ Điều khoản sử dụng
+ Phiên bản %s
+ Phiên bản
+ Thiết lập
+ Tin nhắn
+ Thêm vào màn hình Home
+ Quên
+ Rời Hội thoại
+ Chat trực tiếp
+ Đặt ưu tiên thấp
+ Yêu thích
+ Không
+ Được đề cập và đúng từ khóa
+ Im lặng
+ Được đề cập tới
+ Tất cả tin
+ Tất cả tin (ầm ĩ)
+ Tìm trong danh mục…
+
+ tìm thấy %1$s phòng cho %2$s
+
+
+ %d phòng
+
+ Duyệt danh mục phòng
+ Nhập ID hoặc tên phòng
+ Tham gia phòng
+ Tham gia phòng
+ Tạo phòng
+ Bắt đầu chat
+ LỜI MỜI
+ ƯU TIÊN THẤP
+ PHÒNG
+ YÊU THÍCH
+ DANH MỤC
+ THAM GIA
+ Tìm kiếm trong phòng mã hóa chưa được hỗ trợ.
+ FILES
+ NGƯỜI
+ TIN NHẮN
+ PHÒNG
+ Không có kết quả
+ Lọc người dùng bị cấm
+ Lọc thành viên phòng
+ Tìm kiếm
+ Hủy tải xuống
+ Hủy tải lên
+ Bạn có muốn ẩn tất cả tin nhắn từ người dùng này\?
+\n
+\nLưu ý hành động này sẽ khởi động lại app và có thể tốn thời gian.
+ Lý do báo cáo nội dung này
+ GIA NHẬP
+ ĐƯỢC MỜI
+
+ %d được chọn
+
+ Thiết lập
+ Files
+ Người
+ Chi tiết Phòng
+ Thay đổi chủ đề
+ Nâng cấp phòng
+ Gửi sự kiện m.room.server_acl
+ Thay đổi quyền hạn
+ Thay đổi tên phòng
+ Thay đổi xem lịch sử phòng
+ Bật mã hóa phòng chat
+ Thay đổi địa chỉ chính của phòng
+ Thay đổi hình đại diện phòng
+ Thay đổi widget
+ Thông báo mọi người
+ Xóa tin nhắn gửi bởi người khác
+ Cấm người dùng
+ Loại người dùng
+ Thay đổi thiết lập
+ Mời người dùng
+ Gửi tin nhắn
+ Vai trò mặc định
+ Bạn không đủ quyền để thay đổi vai trò được yêu cầu để thay đổi thiết lập phòng
+ Chọn vai trò được yêu cầu để thay đổi thiết lập của phòng
+ Quyền hạn
+ Việc này có thể có nghĩa là ai đó đang can thiệp vào lưu lượng của bạn, hoặc điện thoại của bạn không tin cậy chứng chỉ được máy chủ trên mạng cung cấp.
+ Không thể xác minh danh tính của máy chủ trên mạng.
+ Mã kiểm tra (%s):
+ Bỏ qua
+ Đăng xuất
+ Không tin cậy
+ Tin cậy
+
+ %d tin nhắn mới
+
+ Bạn không có quyền để đăng vào phòng này.
+ Không tìm thấy tệp
+ Xóa tin nhắn chưa gửi
+ Gửi lại tin nhắn chưa gửi
+ Hủy tất cả
+ Gửi lại tất cả
+ Tin nhắn không được gửi do có phiên làm việc không xác định. %1$s hoặc %2$s ngay\?
+ Tin nhắn không được gửi. %1$s hoặc %2$s ngay\?
+ Kết nối đến máy chủ đã bị mất.
+ Gửi câu trả lời (không mã hóa)…
+ Gửi câu trả lời mã hóa…
+ Gửi tin nhắn (không mã hóa)…
+ Gửi tin nhắn mã hóa…
+ %1$s & %2$s & những người khác đang gõ…
+ %1$s & %2$s đang gõ…
+ %s đang gõ…
+ Tìm kiếm
+ Email hoặc ID Matrix
+ Vui lòng nhập một hoặc nhiều địa chỉ email hoặc ID Matrix
+ Mời người dùng theo ID
+ Chỉ người dùng Matrix
+ THƯ MỤC NGƯỜI DÙNG (%s)
+ LIÊN HỆ CỤC BỘ (%d)
+ Mời theo ID
+ %1$s %2$s
+ %1$s và %2$s
+ "%1$s, "
+ Bạn có chắc bạn muốn mời %s vào cuộc trò chuyện này không\?
+ Lý do
+ Việc hủy cấm người dùng sẽ cho phép họ tham gia lại phòng.
+ Việc cấm người dùng sẽ đá họ ra khỏi phòng này và ngăn họ tham gia lại.
+ Hủy cấm người dùng
+ Lý do cấm
+ Cấm người dùng
+ việc đá người dùng sẽ xóa họ khỏi phòng.
+\n
+\nĐể ngăn họ tham gia lại, bạn nên cấm họ thay vì đá.
+ Lý do đá
+ Đá người dùng
+ Bạn có chắc bạn muốn hủy lời mời đối với người dùng này không\?
+ Hủy lời mời
+ Hủy làm ngơ
+ Việc hủy làm ngơ người dùng này sẽ hiện lại tất cả tin nhắn từ họ.
+ Cuộc gọi âm thanh với %s
+ Cuộc gọi video với %s
+ Cuộc gọi đang reo…
\ No newline at end of file
diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml
index 3e6451b3c7..148790d93a 100644
--- a/vector/src/main/res/values-zh-rCN/strings.xml
+++ b/vector/src/main/res/values-zh-rCN/strings.xml
@@ -2602,6 +2602,7 @@
你是此空间的管理员,请确保你在离开前已将管理权限转让给另一位成员。此空间并非公开空间。你将无法在没有邀请的情况下重新加入。你是这唯一的人。如果你离开,包括你在内的所有人都将无法加入此空间。
+
你确定你想要离开此空间吗?离开空间添加聊天室
diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml
index e05a234dea..b1c60f51fd 100644
--- a/vector/src/main/res/values-zh-rTW/strings.xml
+++ b/vector/src/main/res/values-zh-rTW/strings.xml
@@ -2628,6 +2628,7 @@
空間是將聊天室與人們分組的新方式。歡迎使用空間!新增既有的聊天室與空間
+
您確定您想要離開空間嗎?離開空間新增聊天室
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 9eb51b507f..f3fd04a478 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -903,11 +903,14 @@
Kick userReason to kickkicking user will remove them from this room.\n\nTo prevent them from joining again, you should ban them instead.
+ kicking user will remove them from this space.\n\nTo prevent them from joining again, you should ban them instead.Ban userReason to banUnban userBanning user will kick them from this room and prevent them from joining again.
+ Banning user will kick them from this space and prevent them from joining again.Unbanning user will allow them to join the room again.
+ Unbanning user will allow them to join the space again.Reason
@@ -971,11 +974,15 @@
Room permissions
+ Space permissionsView and update the roles required to change various parts of the room.
+ View and update the roles required to change various parts of the space."Permissions""Select the roles required to change various parts of the room"
+ "Select the roles required to change various parts of this space""You don't have permission to update the roles required to change various parts of the room"
+ "You don't have permission to update the roles required to change various parts of this space"Default roleSend messages
@@ -987,13 +994,18 @@
Notify everyoneModify widgetsChange room avatar
+ Change space avatarChange main address for the room
+ Change main address for the spaceEnable room encryption
+ Enable space encryptionChange history visibilityChange room name
+ Change space nameChange permissionsSend m.room.server_acl eventsUpgrade the room
+ Upgrade the spaceChange topic
@@ -1100,6 +1112,12 @@
Advanced Notification SettingsNotification importance by event
+ Email notification
+ To receive email with notification, please associate an email to your Matrix account
+
+
+ Enable email notifications for %s
+
Default NotificationsMentions and KeywordsOther
@@ -1464,7 +1482,9 @@
Who can read history?Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.Who can access this room?
+ Who can access?Room access
+ Space accessAllow guests to join
@@ -1521,6 +1541,7 @@
Only people invited can find and joinPublicAnyone can find the room and join
+ Anyone can find the space and joinSpace members onlyAnyone in a space with this room can find and join it. Only admins of this room can add it to a space.Members of Space %s can find, preview and join.
@@ -2175,6 +2196,7 @@
Malformed event, cannot displayCreate New Room
+ Create New SpaceNo network. Please check your Internet connection."Change""Change network"
@@ -2279,7 +2301,9 @@
View the room directoryName or ID (#example:matrix.org)
+
Search by name or ID
+ Search by name, ID or mailSearch Name
@@ -2316,6 +2340,7 @@
Identity serverDisconnect identity serverConfigure identity server
+ Open Discovery SettingsChange identity serverYou are currently using %1$s to discover and be discoverable by existing contacts you know.You are not currently using an identity server. To discover and be discoverable by existing contacts you know, configure one below.
@@ -2538,6 +2563,7 @@
EmailEmail (optional)Next
+ Doesn\'t look like a valid email addressSet phone numberSet a phone number to optionally allow people you know to discover you.
@@ -2641,6 +2667,9 @@
${app_name} may crash more often when an unexpected error occurs
+ Show debug info on screen
+ Show some useful info to help debugging the application
+
Prepends ¯\\_(ツ)_/¯ to a plain-text message"Enable encryption"
@@ -2658,6 +2687,7 @@
Please provide a room addressSome characters are not allowedCreating room…
+ Creating space…Your email domain is not authorized to register on this server
@@ -3385,6 +3415,7 @@
Event contentCreate a Space
+ Add to the given SpaceJoin the Space with the given idLeave room with given id (or current room if null)Upgrades a room to a new version
@@ -3423,6 +3454,8 @@
Give it a name to continue.What are some discussions you want to have in %s?We’ll create rooms for them. You can add more later too.
+ Who are your teammates?
+ Ensure the right people have access to %s company. You can invite more later.What things are you working on?Let’s create a room for each of them. You can add more later too, including already existing ones.General
@@ -3435,6 +3468,7 @@
It’s just you at the moment. %s will be even better with others.Invite by emailInvite by username
+ Invite by username or mailShare linkInvite to %s"They’ll be able to explore %s"
@@ -3448,6 +3482,17 @@
Join AnywayThis alias is not accessible at this time.\nTry again later, or ask a room admin to check if you have access.
+
+ You are not currently using an identity server. In order to invite teammates and be discoverable by them, configure one below.
+
+
+ Finish setting up discovery.
+ Invite by email, find contacts and more…
+ Finish setup
+
+ Discovery (%s)
+
+
You’re not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right.Welcome to %1$s, %2$s.
@@ -3460,13 +3505,22 @@
Explore roomsAdd roomsLeave Space
+ Are you sure you want to leave %s?
+
Are you sure you want to leave the space?You are the only person here. If you leave, no one will be able to join in the future, including you.
- This space is not public. You will not be able to rejoin without an invite.
- You are admin of this space, ensure that you have transferred admin right to another member before leaving.
-
+ You won\'t be able to rejoin unless you are re-invited.
+ You\'re the only admin of this space. Leaving it will mean no one has control over it.
+ Leave all rooms and spaces
+ You will leave all rooms and spaces in %s.
+ Don’t leave any rooms and spaces
+ Leave specific rooms and spaces…
+ Pick things to leaveAdd existing rooms and space
+ Add existing rooms
+ Add existing spaces
+ Add a space to any space you manage.Add roomsWelcome to Spaces!Spaces are a new way to group rooms and people.
diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
index 9c9a25c7ed..c65ded7c32 100644
--- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml
+++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
@@ -6,8 +6,8 @@
@@ -17,6 +17,13 @@
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
+
+
+
+
@@ -114,7 +118,6 @@
android:key="SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
android:title="@string/settings_start_on_boot" />
-