Merge branch 'release/0.4.12'
This commit is contained in:
commit
64b6dd1fca
2159 changed files with 15530 additions and 7504 deletions
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
|
|
@ -9,8 +9,8 @@ on:
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
env:
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx8g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
debug:
|
debug:
|
||||||
|
|
@ -41,21 +41,28 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug Gplay APK
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
run: ./gradlew :app:assembleGplayDebug :app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload APK APKs
|
- name: Assemble debug Fdroid APK
|
||||||
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
|
env:
|
||||||
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
run: ./gradlew app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Upload debug APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/gplay/debug/*.apk
|
app/build/outputs/apk/gplay/debug/*-universal-debug.apk
|
||||||
app/build/outputs/apk/fdroid/debug/*.apk
|
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
|
||||||
- uses: rnkdsh/action-upload-diawi@v1.5.5
|
- uses: rnkdsh/action-upload-diawi@v1.5.5
|
||||||
id: diawi
|
id: diawi
|
||||||
# Do not fail the whole build if Diawi upload fails
|
# Do not fail the whole build if Diawi upload fails
|
||||||
|
|
|
||||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@11.3.1
|
uses: danger/danger-js@12.2.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
4
.github/workflows/generate_github_pages.yml
vendored
4
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -22,10 +22,10 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Run World screenshots generation script
|
- name: Run World screenshots generation script
|
||||||
run: |
|
run: |
|
||||||
./tools/test/generateWorldScreenshots.py
|
./tools/test/generateWorldScreenshots.py
|
||||||
|
|
|
||||||
40
.github/workflows/maestro.yml
vendored
40
.github/workflows/maestro.yml
vendored
|
|
@ -12,12 +12,10 @@ env:
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
maestro-cloud:
|
build-apk:
|
||||||
name: Maestro test suite
|
name: Build APK
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
# Allow one per PR.
|
# Allow one per PR.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('maestro-{0}', github.ref) }}
|
group: ${{ format('maestro-{0}', github.ref) }}
|
||||||
|
|
@ -41,19 +39,49 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
run: ./gradlew :app:assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
- name: Upload APK as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: elementx-apk-maestro
|
||||||
|
path: |
|
||||||
|
app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
|
||||||
|
retention-days: 5
|
||||||
|
overwrite: true
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
maestro-cloud:
|
||||||
|
name: Maestro test suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-apk
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
||||||
|
# Allow one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ format('maestro-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Download APK artifact from previous job
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: elementx-apk-maestro
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
||||||
app-file: app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
|
app-file: app-gplay-x86_64-debug.apk
|
||||||
env: |
|
env: |
|
||||||
MAESTRO_USERNAME=maestroelement
|
MAESTRO_USERNAME=maestroelement
|
||||||
MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||||
|
|
|
||||||
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
|
|
@ -26,10 +26,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Search for invalid screenshot files
|
- name: Search for invalid screenshot files
|
||||||
run: ./tools/test/checkInvalidScreenshots.py
|
run: ./tools/test/checkInvalidScreenshots.py
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@11.3.1
|
uses: danger/danger-js@12.2.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
5
.github/workflows/sync-localazy.yml
vendored
5
.github/workflows/sync-localazy.yml
vendored
|
|
@ -21,10 +21,10 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Setup Localazy
|
- name: Setup Localazy
|
||||||
run: |
|
run: |
|
||||||
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
|
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
|
||||||
|
|
@ -33,6 +33,7 @@ jobs:
|
||||||
- name: Run Localazy script
|
- name: Run Localazy script
|
||||||
run: |
|
run: |
|
||||||
./tools/localazy/downloadStrings.sh --all
|
./tools/localazy/downloadStrings.sh --all
|
||||||
|
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||||
./tools/test/generateAllScreenshots.py
|
./tools/test/generateAllScreenshots.py
|
||||||
- name: Create Pull Request for Strings
|
- name: Create Pull Request for Strings
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
|
|
||||||
4
.github/workflows/sync-sas-strings.yml
vendored
4
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -13,10 +13,10 @@ jobs:
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Install Prerequisite dependencies
|
- name: Install Prerequisite dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install requests
|
pip install requests
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- tapOn: "Create"
|
- tapOn: "Create"
|
||||||
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
||||||
- tapOn: "aRoomName"
|
- tapOn: "aRoomName"
|
||||||
- tapOn: "Invite people"
|
- tapOn: "Invite"
|
||||||
# assert there's 1 member and 1 invitee
|
# assert there's 1 member and 1 invitee
|
||||||
- tapOn: "Search for someone"
|
- tapOn: "Search for someone"
|
||||||
- inputText: ${MAESTRO_INVITEE2_MXID}
|
- inputText: ${MAESTRO_INVITEE2_MXID}
|
||||||
|
|
|
||||||
42
CHANGES.md
42
CHANGES.md
|
|
@ -1,3 +1,41 @@
|
||||||
|
Changes in Element X v0.4.12 (2024-05-13)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Add support for expected decryption errors due to membership (UX and analytics). ([#2754](https://github.com/element-hq/element-x-android/issues/2754))
|
||||||
|
- Handle permalink navigation to Events. ([#2759](https://github.com/element-hq/element-x-android/issues/2759))
|
||||||
|
- Pretty-print event JSON in debug viewer ([#2771](https://github.com/element-hq/element-x-android/issues/2771))
|
||||||
|
- Add support for external permalinks. ([#2776](https://github.com/element-hq/element-x-android/issues/2776))
|
||||||
|
- Enable support for Android per-app language preferences ([#2795](https://github.com/element-hq/element-x-android/issues/2795))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix session verification being asked again for already verified users. ([#2718](https://github.com/element-hq/element-x-android/issues/2718))
|
||||||
|
- Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen. ([#2740](https://github.com/element-hq/element-x-android/issues/2740))
|
||||||
|
- Adjust the typography used in the selected user component so a user's display name fits better. ([#2760](https://github.com/element-hq/element-x-android/issues/2760))
|
||||||
|
- User display name overflows in timeline messages when it's way too long. ([#2761](https://github.com/element-hq/element-x-android/issues/2761))
|
||||||
|
- Ensure the application open the room when a notification is clicked. ([#2778](https://github.com/element-hq/element-x-android/issues/2778))
|
||||||
|
- Enforce mandatory session verification only for new logins. ([#2810](https://github.com/element-hq/element-x-android/issues/2810))
|
||||||
|
- Make log less verbose, make sure we upload as many log files as possible before reaching the request size limit of the bug reporting service, discard older logs if they don't fit. ([#2825](https://github.com/element-hq/element-x-android/issues/2825))
|
||||||
|
- Remove 'Join' button in room directory search results. ([#2827](https://github.com/element-hq/element-x-android/issues/2827))
|
||||||
|
- Add missing `app_id` and `Version` properties to bug reports. ([#2829](https://github.com/element-hq/element-x-android/issues/2829))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- RoomMember screen: fallback to userProfile data, if the member is not a user of the room. ([#2721](https://github.com/element-hq/element-x-android/issues/2721))
|
||||||
|
- Migrate application data. ([#2749](https://github.com/element-hq/element-x-android/issues/2749))
|
||||||
|
- Let the SDK manage the file log cleanup, and keep one week of log. ([#2758](https://github.com/element-hq/element-x-android/issues/2758))
|
||||||
|
- UX cleanup: reorder options in the main settings screen. ([#2801](https://github.com/element-hq/element-x-android/issues/2801))
|
||||||
|
- Analytics: Add support to report current session verification and recovery state ([#2806](https://github.com/element-hq/element-x-android/issues/2806))
|
||||||
|
- UX cleanup: room details screen, add new CTA buttons for Invite and Call actions. ([#2814](https://github.com/element-hq/element-x-android/issues/2814))
|
||||||
|
- UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too. ([#2818](https://github.com/element-hq/element-x-android/issues/2818))
|
||||||
|
- Add room badges to room details screen. ([#2822](https://github.com/element-hq/element-x-android/issues/2822))
|
||||||
|
|
||||||
|
Security
|
||||||
|
-------------
|
||||||
|
- Bump the Rust SDK to `v0.2.18` to remediate [CVE-2024-34353 / GHSA-9ggc-845v-gcgv](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-9ggc-845v-gcgv).
|
||||||
|
|
||||||
Changes in Element X v0.4.10 (2024-04-17)
|
Changes in Element X v0.4.10 (2024-04-17)
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
|
|
@ -20,6 +58,10 @@ Changes in Element X v0.4.9 (2024-04-12)
|
||||||
|
|
||||||
- Synchronize Localazy Strings.
|
- Synchronize Localazy Strings.
|
||||||
|
|
||||||
|
Security
|
||||||
|
----------
|
||||||
|
- Fix crash while processing a room message containing a malformed pill.
|
||||||
|
|
||||||
Changes in Element X v0.4.8 (2024-04-10)
|
Changes in Element X v0.4.8 (2024-04-10)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import extension.allServicesImpl
|
||||||
import extension.gitBranchName
|
import extension.gitBranchName
|
||||||
import extension.gitRevision
|
import extension.gitRevision
|
||||||
import extension.koverDependencies
|
import extension.koverDependencies
|
||||||
|
import extension.locales
|
||||||
import extension.setupKover
|
import extension.setupKover
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -74,6 +75,10 @@ android {
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
resourceConfigurations += locales
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
@ -219,6 +224,7 @@ dependencies {
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir, logger)
|
allFeaturesImpl(rootDir, logger)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call)
|
||||||
|
implementation(projects.features.migration.api)
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:localeConfig="@xml/locales_config"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|
@ -74,6 +75,51 @@
|
||||||
|
|
||||||
<data android:scheme="io.element" />
|
<data android:scheme="io.element" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
Element web links
|
||||||
|
-->
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
|
||||||
|
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="app.element.io" />
|
||||||
|
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="develop.element.io" />
|
||||||
|
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="staging.element.io" />
|
||||||
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
matrix.to links
|
||||||
|
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
|
||||||
|
https://developer.android.com/training/app-links#web-links
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="matrix.to" />
|
||||||
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
links from matrix.to website
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="element" />
|
||||||
|
<data android:host="user" />
|
||||||
|
<data android:host="room" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ class MainActivity : NodeActivity() {
|
||||||
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
|
val migrationState = appBindings.migrationEntryPoint().present()
|
||||||
ElementTheme(
|
ElementTheme(
|
||||||
darkTheme = theme.isDark()
|
darkTheme = theme.isDark()
|
||||||
) {
|
) {
|
||||||
|
|
@ -98,6 +99,21 @@ class MainActivity : NodeActivity() {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
|
if (migrationState.migrationAction.isSuccess()) {
|
||||||
|
MainNodeHost()
|
||||||
|
} else {
|
||||||
|
appBindings.migrationEntryPoint().Render(
|
||||||
|
state = migrationState,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainNodeHost() {
|
||||||
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||||
MainNode(
|
MainNode(
|
||||||
it,
|
it,
|
||||||
|
|
@ -114,14 +130,12 @@ class MainActivity : NodeActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when:
|
* Called when:
|
||||||
* - the launcher icon is clicked (if the app is already running);
|
* - the launcher icon is clicked (if the app is already running);
|
||||||
* - a notification is clicked.
|
* - a notification is clicked.
|
||||||
|
* - a deep link have been clicked
|
||||||
* - the app is going to background (<- this is strange)
|
* - the app is going to background (<- this is strange)
|
||||||
*/
|
*/
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.x.di
|
package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import io.element.android.features.api.MigrationEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
|
|
@ -35,4 +36,6 @@ interface AppBindings {
|
||||||
fun lockScreenService(): LockScreenService
|
fun lockScreenService(): LockScreenService
|
||||||
|
|
||||||
fun preferencesStore(): AppPreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
|
|
||||||
|
fun migrationEntryPoint(): MigrationEntryPoint
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,13 @@ class TracingInitializer : Initializer<Unit> {
|
||||||
writesToLogcat = false,
|
writesToLogcat = false,
|
||||||
writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
|
writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
|
||||||
directory = bugReporter.logDirectory().absolutePath,
|
directory = bugReporter.logDirectory().absolutePath,
|
||||||
filenamePrefix = "logs"
|
filenamePrefix = "logs",
|
||||||
|
filenameSuffix = null,
|
||||||
|
// Keep a minimum of 1 week of log files.
|
||||||
|
numberOfFiles = 7 * 24,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bugReporter.cleanLogDirectoryIfNeeded()
|
|
||||||
bugReporter.setCurrentTracingFilter(tracingConfiguration.filterConfiguration.filter)
|
bugReporter.setCurrentTracingFilter(tracingConfiguration.filterConfiguration.filter)
|
||||||
tracingService.setupTracing(tracingConfiguration)
|
tracingService.setupTracing(tracingConfiguration)
|
||||||
// Also set env variable for rust back trace
|
// Also set env variable for rust back trace
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,4 @@ class IntentProviderImpl @Inject constructor(
|
||||||
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
|
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInviteListIntent(sessionId: SessionId): Intent {
|
|
||||||
return Intent(context, MainActivity::class.java).apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
data = deepLinkCreator.inviteList(sessionId).toUri()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
app/src/main/res/resources.properties
Normal file
17
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
unqualifiedResLocale=en
|
||||||
19
app/src/main/res/xml/locales_config.xml
Normal file
19
app/src/main/res/xml/locales_config.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit -->
|
||||||
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<locale android:name="be"/>
|
||||||
|
<locale android:name="bg"/>
|
||||||
|
<locale android:name="cs"/>
|
||||||
|
<locale android:name="de"/>
|
||||||
|
<locale android:name="en"/>
|
||||||
|
<locale android:name="es"/>
|
||||||
|
<locale android:name="fr"/>
|
||||||
|
<locale android:name="hu"/>
|
||||||
|
<locale android:name="in"/>
|
||||||
|
<locale android:name="it"/>
|
||||||
|
<locale android:name="ro"/>
|
||||||
|
<locale android:name="ru"/>
|
||||||
|
<locale android:name="sk"/>
|
||||||
|
<locale android:name="sv"/>
|
||||||
|
<locale android:name="uk"/>
|
||||||
|
<locale android:name="zh-TW"/>
|
||||||
|
</locale-config>
|
||||||
|
|
@ -67,16 +67,6 @@ class IntentProviderImplTest {
|
||||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test getInviteListIntent`() {
|
|
||||||
val sut = createIntentProviderImpl()
|
|
||||||
val result = sut.getInviteListIntent(
|
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
)
|
|
||||||
result.commonAssertions()
|
|
||||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/invites")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createIntentProviderImpl(): IntentProviderImpl {
|
private fun createIntentProviderImpl(): IntentProviderImpl {
|
||||||
return IntentProviderImpl(
|
return IntentProviderImpl(
|
||||||
context = RuntimeEnvironment.getApplication() as Context,
|
context = RuntimeEnvironment.getApplication() as Context,
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,9 @@ object ApplicationConfig {
|
||||||
* For Element, the value is "Element". We use the same name for desktop and mobile for now.
|
* For Element, the value is "Element". We use the same name for desktop and mobile for now.
|
||||||
*/
|
*/
|
||||||
const val DESKTOP_APPLICATION_NAME: String = "Element"
|
const val DESKTOP_APPLICATION_NAME: String = "Element"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum size of the upload request. Default value is just below CloudFlare's max request size.
|
||||||
|
*/
|
||||||
|
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ dependencies {
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
testImplementation(projects.features.rageshake.impl)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(libs.test.appyx.junit)
|
testImplementation(libs.test.appyx.junit)
|
||||||
testImplementation(libs.test.arch.core)
|
testImplementation(libs.test.arch.core)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,8 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -48,7 +46,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
|
@ -59,20 +56,23 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
|
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.waitForChildAttached
|
import io.element.android.libraries.architecture.waitForNavTargetAttached
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
|
@ -81,7 +81,6 @@ import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -95,11 +94,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
private val inviteListEntryPoint: InviteListEntryPoint,
|
private val userProfileEntryPoint: UserProfileEntryPoint,
|
||||||
private val ftueEntryPoint: FtueEntryPoint,
|
private val ftueEntryPoint: FtueEntryPoint,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
private val lockScreenStateService: LockScreenService,
|
private val lockScreenStateService: LockScreenService,
|
||||||
|
|
@ -160,23 +158,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
observeSyncStateAndNetworkStatus()
|
observeSyncStateAndNetworkStatus()
|
||||||
observeInvitesLoadingState()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeInvitesLoadingState() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
matrixClient.roomListService.invites.loadingState
|
|
||||||
.collect { inviteState ->
|
|
||||||
when (inviteState) {
|
|
||||||
is RoomList.LoadingState.Loaded -> if (inviteState.numberOfRooms == 0) {
|
|
||||||
backstack.removeLast(NavTarget.InviteList)
|
|
||||||
}
|
|
||||||
RoomList.LoadingState.NotLoaded -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|
@ -215,9 +196,14 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
val roomId: RoomId,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: RoomDescription? = null,
|
val roomDescription: RoomDescription? = null,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class UserProfile(
|
||||||
|
val userId: UserId,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -233,9 +219,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object InviteList : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Ftue : NavTarget
|
data object Ftue : NavTarget
|
||||||
|
|
||||||
|
|
@ -257,7 +240,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.RoomList -> {
|
NavTarget.RoomList -> {
|
||||||
val callback = object : RoomListEntryPoint.Callback {
|
val callback = object : RoomListEntryPoint.Callback {
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSettingsClicked() {
|
override fun onSettingsClicked() {
|
||||||
|
|
@ -272,12 +255,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvitesClicked() {
|
|
||||||
backstack.push(NavTarget.InviteList)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReportBugClicked() {
|
override fun onReportBugClicked() {
|
||||||
|
|
@ -296,11 +275,33 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
is NavTarget.Room -> {
|
is NavTarget.Room -> {
|
||||||
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
val callback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||||
override fun onOpenRoom(roomId: RoomId) {
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
coroutineScope.launch { attachRoom(roomId) }
|
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
|
when (data) {
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
// Should not happen (handled by MessagesNode)
|
||||||
|
Timber.e("User link clicked: ${data.userId}.")
|
||||||
|
}
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.Room(
|
||||||
|
roomIdOrAlias = data.roomIdOrAlias,
|
||||||
|
initialElement = RoomNavigationTarget.Messages(data.eventId),
|
||||||
|
// TODO Use the viaParameters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PermalinkData.FallbackLink,
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
|
// Should not happen (handled by MessagesNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenGlobalNotificationSettings() {
|
override fun onOpenGlobalNotificationSettings() {
|
||||||
|
|
@ -308,12 +309,23 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = RoomFlowNode.Inputs(
|
val inputs = RoomFlowNode.Inputs(
|
||||||
roomId = navTarget.roomId,
|
roomIdOrAlias = navTarget.roomIdOrAlias,
|
||||||
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
||||||
initialElement = navTarget.initialElement
|
initialElement = navTarget.initialElement
|
||||||
)
|
)
|
||||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
|
is NavTarget.UserProfile -> {
|
||||||
|
val callback = object : UserProfileEntryPoint.Callback {
|
||||||
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userProfileEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
is NavTarget.Settings -> {
|
is NavTarget.Settings -> {
|
||||||
val callback = object : PreferencesEntryPoint.Callback {
|
val callback = object : PreferencesEntryPoint.Callback {
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
|
|
@ -325,11 +337,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||||
return preferencesEntryPoint.nodeBuilder(this, buildContext)
|
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(inputs)
|
.params(inputs)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -337,7 +349,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.CreateRoom -> {
|
NavTarget.CreateRoom -> {
|
||||||
val callback = object : CreateRoomEntryPoint.Callback {
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
override fun onSuccess(roomId: RoomId) {
|
override fun onSuccess(roomId: RoomId) {
|
||||||
backstack.replace(NavTarget.Room(roomId))
|
backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,43 +363,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.InviteList -> {
|
|
||||||
val callback = object : InviteListEntryPoint.Callback {
|
|
||||||
override fun onBackClicked() {
|
|
||||||
backstack.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInviteClicked(roomId: RoomId) {
|
|
||||||
backstack.push(NavTarget.Room(roomId))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInviteAccepted(roomId: RoomId) {
|
|
||||||
backstack.push(NavTarget.Room(roomId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inviteListEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.callback(callback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
NavTarget.Ftue -> {
|
NavTarget.Ftue -> {
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : FtueEntryPoint.Callback {
|
|
||||||
override fun onFtueFlowFinished() {
|
|
||||||
lifecycleScope.launch { attachRoomList() }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.RoomDirectorySearch -> {
|
NavTarget.RoomDirectorySearch -> {
|
||||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||||
override fun onRoomJoined(roomId: RoomId) {
|
override fun onRoomJoined(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResultClicked(roomDescription: RoomDescription) {
|
override fun onResultClicked(roomDescription: RoomDescription) {
|
||||||
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
|
backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -395,42 +383,42 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoomList() {
|
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
|
||||||
if (!canShowRoomList()) return
|
waitForNavTargetAttached { navTarget ->
|
||||||
attachChild<Node> {
|
navTarget is NavTarget.RoomList
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun attachRoom(roomId: RoomId) {
|
|
||||||
if (!canShowRoomList()) return
|
|
||||||
attachChild<RoomFlowNode> {
|
attachChild<RoomFlowNode> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.push(
|
||||||
backstack.push(NavTarget.Room(roomId))
|
NavTarget.Room(
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
initialElement = RoomNavigationTarget.Messages(
|
||||||
|
focusedEventId = eventId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
suspend fun attachUser(userId: UserId) {
|
||||||
if (!canShowRoomList()) return@withContext
|
waitForNavTargetAttached { navTarget ->
|
||||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
navTarget is NavTarget.RoomList
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
|
||||||
backstack.push(NavTarget.InviteList)
|
|
||||||
waitForChildAttached<Node, NavTarget> { navTarget ->
|
|
||||||
navTarget is NavTarget.InviteList
|
|
||||||
}
|
}
|
||||||
|
attachChild<Node> {
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.UserProfile(
|
||||||
|
userId = userId,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canShowRoomList(): Boolean {
|
|
||||||
return ftueService.state.value is FtueState.Complete
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
||||||
val isFtueDisplayed by ftueService.state.collectAsState()
|
val ftueState by ftueService.state.collectAsState()
|
||||||
BackstackView()
|
BackstackView()
|
||||||
if (isFtueDisplayed is FtueState.Complete) {
|
if (ftueState is FtueState.Complete) {
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
}
|
}
|
||||||
if (lockScreenState == LockScreenLockState.Locked) {
|
if (lockScreenState == LockScreenLockState.Locked) {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -279,18 +281,37 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
when (resolvedIntent) {
|
when (resolvedIntent) {
|
||||||
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
||||||
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||||
|
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun navigateTo(permalinkData: PermalinkData) {
|
||||||
|
Timber.d("Navigating to $permalinkData")
|
||||||
|
attachSession(null)
|
||||||
|
.apply {
|
||||||
|
when (permalinkData) {
|
||||||
|
is PermalinkData.FallbackLink -> Unit
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
attachRoom(
|
||||||
|
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||||
|
eventId = permalinkData.eventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
attachUser(permalinkData.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
Timber.d("Navigating to $deeplinkData")
|
Timber.d("Navigating to $deeplinkData")
|
||||||
attachSession(deeplinkData.sessionId)
|
attachSession(deeplinkData.sessionId)
|
||||||
.attachSession()
|
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoomList()
|
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
|
||||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -299,10 +320,12 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
oidcActionFlow.post(oidcAction)
|
oidcActionFlow.post(oidcAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
// [sessionId] will be null for permalink.
|
||||||
|
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
|
||||||
// TODO handle multi-session
|
// TODO handle multi-session
|
||||||
return waitForChildAttached { navTarget ->
|
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
|
||||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
|
||||||
}
|
}
|
||||||
|
.attachSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface ResolvedIntent {
|
sealed interface ResolvedIntent {
|
||||||
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
|
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
|
||||||
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
||||||
|
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntentResolver @Inject constructor(
|
class IntentResolver @Inject constructor(
|
||||||
private val deeplinkParser: DeeplinkParser,
|
private val deeplinkParser: DeeplinkParser,
|
||||||
private val oidcIntentResolver: OidcIntentResolver
|
private val oidcIntentResolver: OidcIntentResolver,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
fun resolve(intent: Intent): ResolvedIntent? {
|
fun resolve(intent: Intent): ResolvedIntent? {
|
||||||
if (intent.canBeIgnored()) return null
|
if (intent.canBeIgnored()) return null
|
||||||
|
|
||||||
|
// Coming from a notification?
|
||||||
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
||||||
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
||||||
|
|
||||||
|
// Coming during login using Oidc?
|
||||||
val oidcAction = oidcIntentResolver.resolve(intent)
|
val oidcAction = oidcIntentResolver.resolve(intent)
|
||||||
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
||||||
|
|
||||||
|
// External link clicked? (matrix.to, element.io, etc.)
|
||||||
|
val permalinkData = intent
|
||||||
|
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||||
|
?.dataString
|
||||||
|
?.let { permalinkParser.parse(it) }
|
||||||
|
?.takeIf { it !is PermalinkData.FallbackLink }
|
||||||
|
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
|
||||||
|
|
||||||
// Unknown intent
|
// Unknown intent
|
||||||
Timber.w("Unknown intent")
|
Timber.w("Unknown intent")
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
|
||||||
|
fun SessionVerifiedStatus.toAnalyticsUserPropertyValue(): UserProperties.VerificationState? {
|
||||||
|
return when (this) {
|
||||||
|
// we don't need to report transient states
|
||||||
|
SessionVerifiedStatus.Unknown -> null
|
||||||
|
SessionVerifiedStatus.NotVerified -> UserProperties.VerificationState.NotVerified
|
||||||
|
SessionVerifiedStatus.Verified -> UserProperties.VerificationState.Verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecoveryState.toAnalyticsUserPropertyValue(): UserProperties.RecoveryState? {
|
||||||
|
return when (this) {
|
||||||
|
RecoveryState.ENABLED -> UserProperties.RecoveryState.Enabled
|
||||||
|
RecoveryState.DISABLED -> UserProperties.RecoveryState.Disabled
|
||||||
|
RecoveryState.INCOMPLETE -> UserProperties.RecoveryState.Incomplete
|
||||||
|
// we don't need to report transient states
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun SessionVerifiedStatus.toAnalyticsStateChangeValue(): CryptoSessionStateChange.VerificationState? {
|
||||||
|
return when (this) {
|
||||||
|
// we don't need to report transient states
|
||||||
|
SessionVerifiedStatus.Unknown -> null
|
||||||
|
SessionVerifiedStatus.NotVerified -> CryptoSessionStateChange.VerificationState.NotVerified
|
||||||
|
SessionVerifiedStatus.Verified -> CryptoSessionStateChange.VerificationState.Verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecoveryState.toAnalyticsStateChangeValue(): CryptoSessionStateChange.RecoveryState? {
|
||||||
|
return when (this) {
|
||||||
|
RecoveryState.ENABLED -> CryptoSessionStateChange.RecoveryState.Enabled
|
||||||
|
RecoveryState.DISABLED -> CryptoSessionStateChange.RecoveryState.Disabled
|
||||||
|
RecoveryState.INCOMPLETE -> CryptoSessionStateChange.RecoveryState.Incomplete
|
||||||
|
// we don't need to report transient states
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,14 +22,19 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -38,6 +43,8 @@ class LoggedInPresenter @Inject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val encryptionService: EncryptionService,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
|
|
@ -62,8 +69,36 @@ class LoggedInPresenter @Inject constructor(
|
||||||
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val verificationState by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||||
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
|
LaunchedEffect(verificationState, recoveryState) {
|
||||||
|
reportCryptoStatusToAnalytics(verificationState, recoveryState)
|
||||||
|
}
|
||||||
|
|
||||||
return LoggedInState(
|
return LoggedInState(
|
||||||
showSyncSpinner = showSyncSpinner,
|
showSyncSpinner = showSyncSpinner,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reportCryptoStatusToAnalytics(verificationState: SessionVerifiedStatus, recoveryState: RecoveryState) {
|
||||||
|
// Update first the user property, to store the current status for that posthog user
|
||||||
|
val userVerificationState = verificationState.toAnalyticsUserPropertyValue()
|
||||||
|
val userRecoveryState = recoveryState.toAnalyticsUserPropertyValue()
|
||||||
|
if (userRecoveryState != null && userVerificationState != null) {
|
||||||
|
// we want to report when both value are known (if one is unknown we wait until we have them both)
|
||||||
|
analyticsService.updateUserProperties(
|
||||||
|
UserProperties(
|
||||||
|
verificationState = userVerificationState,
|
||||||
|
recoveryState = userRecoveryState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also report when there is a change in the state, to be able to track the changes
|
||||||
|
val changeVerificationState = verificationState.toAnalyticsStateChangeValue()
|
||||||
|
val changeRecoveryState = recoveryState.toAnalyticsStateChangeValue()
|
||||||
|
if (changeVerificationState != null && changeRecoveryState != null) {
|
||||||
|
analyticsService.capture(CryptoSessionStateChange(changeRecoveryState, changeVerificationState))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,9 @@
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -36,22 +35,30 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
|
import io.element.android.appnav.room.joined.LoadingRoomNodeView
|
||||||
|
import io.element.android.appnav.room.joined.LoadingRoomState
|
||||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -62,8 +69,9 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
|
||||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||||
|
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Loading,
|
initialElement = NavTarget.Loading,
|
||||||
|
|
@ -73,9 +81,9 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: Optional<RoomDescription>,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -85,54 +93,108 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
data object Loading : NavTarget
|
data object Loading : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object JoinRoom : NavTarget
|
data class Resolving(val roomAlias: RoomAlias) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object JoinedRoom : NavTarget
|
data class JoinRoom(val roomId: RoomId) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
client.getRoomInfoFlow(
|
resolveRoomId()
|
||||||
inputs.roomId
|
|
||||||
).onEach { roomInfo ->
|
|
||||||
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
|
|
||||||
if (roomInfo.getOrNull()?.currentUserMembership == CurrentUserMembership.JOINED) {
|
|
||||||
backstack.newRoot(NavTarget.JoinedRoom)
|
|
||||||
} else {
|
|
||||||
backstack.newRoot(NavTarget.JoinRoom)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
|
|
||||||
// When leaving the room from this session only, navigate up.
|
private fun resolveRoomId() {
|
||||||
roomMembershipObserver.updates
|
lifecycleScope.launch {
|
||||||
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
|
when (val i = inputs.roomIdOrAlias) {
|
||||||
.onEach {
|
is RoomIdOrAlias.Alias -> {
|
||||||
|
backstack.newRoot(NavTarget.Resolving(i.roomAlias))
|
||||||
|
}
|
||||||
|
is RoomIdOrAlias.Id -> {
|
||||||
|
subscribeToRoomInfoFlow(i.roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribeToRoomInfoFlow(roomId: RoomId) {
|
||||||
|
val roomInfoFlow = client.getRoomInfoFlow(
|
||||||
|
roomId = roomId
|
||||||
|
).map { it.getOrNull() }
|
||||||
|
|
||||||
|
val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged()
|
||||||
|
val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged()
|
||||||
|
combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
|
||||||
|
Timber.d("Room membership: $membership")
|
||||||
|
when (membership) {
|
||||||
|
CurrentUserMembership.JOINED -> {
|
||||||
|
if (isSpace) {
|
||||||
|
// It should not happen, but probably due to an issue in the sliding sync,
|
||||||
|
// we can have a space here in case the space has just been joined.
|
||||||
|
// So navigate to the JoinRoom target for now, which will
|
||||||
|
// handle the space not supported screen
|
||||||
|
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
||||||
|
} else {
|
||||||
|
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CurrentUserMembership.LEFT -> {
|
||||||
|
// Left the room, navigate out of this flow
|
||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
else -> {
|
||||||
|
// Was invited or the room is not known, display the join room screen
|
||||||
|
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Loading -> loadingNode(buildContext)
|
is NavTarget.Loading -> loadingNode(buildContext)
|
||||||
NavTarget.JoinRoom -> {
|
is NavTarget.Resolving -> {
|
||||||
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
|
val callback = object : RoomAliasResolverEntryPoint.Callback {
|
||||||
|
override fun onAliasResolved(roomId: RoomId) {
|
||||||
|
subscribeToRoomInfoFlow(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
|
||||||
|
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(callback)
|
||||||
|
.params(params)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
is NavTarget.JoinRoom -> {
|
||||||
|
val inputs = JoinRoomEntryPoint.Inputs(
|
||||||
|
roomId = navTarget.roomId,
|
||||||
|
roomIdOrAlias = inputs.roomIdOrAlias,
|
||||||
|
roomDescription = inputs.roomDescription,
|
||||||
|
)
|
||||||
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||||
}
|
}
|
||||||
NavTarget.JoinedRoom -> {
|
is NavTarget.JoinedRoom -> {
|
||||||
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||||
val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
|
val inputs = JoinedRoomFlowNode.Inputs(
|
||||||
|
roomId = navTarget.roomId,
|
||||||
|
initialElement = inputs.initialElement
|
||||||
|
)
|
||||||
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
|
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
||||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||||
CircularProgressIndicator()
|
LoadingRoomNodeView(
|
||||||
}
|
state = LoadingRoomState.Loading,
|
||||||
|
hasNetworkConnection = networkStatus == NetworkStatus.Online,
|
||||||
|
onBackClicked = { navigateUp() },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,17 @@
|
||||||
|
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room
|
||||||
|
|
||||||
enum class RoomNavigationTarget {
|
import android.os.Parcelable
|
||||||
Messages,
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
Details,
|
import kotlinx.parcelize.Parcelize
|
||||||
NotificationSettings,
|
|
||||||
|
sealed interface RoomNavigationTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object Details : RoomNavigationTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object NotificationSettings : RoomNavigationTarget
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class JoinedRoomFlowNode @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -106,7 +106,10 @@ class JoinedRoomFlowNode @AssistedInject constructor(
|
||||||
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||||
val awaitRoomState = loadingRoomStateStateFlow.value
|
val awaitRoomState = loadingRoomStateStateFlow.value
|
||||||
if (awaitRoomState is LoadingRoomState.Loaded) {
|
if (awaitRoomState is LoadingRoomState.Loaded) {
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(
|
||||||
|
room = awaitRoomState.room,
|
||||||
|
initialElement = inputs.initialElement
|
||||||
|
)
|
||||||
createNode<JoinedRoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
createNode<JoinedRoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||||
} else {
|
} else {
|
||||||
loadingNode(buildContext, this::navigateUp)
|
loadingNode(buildContext, this::navigateUp)
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,10 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -63,8 +65,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
initialElement = when (val input = plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
||||||
RoomNavigationTarget.Messages -> NavTarget.Messages
|
is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
|
||||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
||||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
||||||
},
|
},
|
||||||
|
|
@ -75,13 +77,14 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
), DaggerComponentOwner {
|
), DaggerComponentOwner {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId)
|
fun onOpenRoom(roomId: RoomId)
|
||||||
|
fun onPermalinkClicked(data: PermalinkData)
|
||||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||||
fun onOpenGlobalNotificationSettings()
|
fun onOpenGlobalNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val room: MatrixRoom,
|
val room: MatrixRoom,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
|
val initialElement: RoomNavigationTarget,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
@ -139,7 +142,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Messages -> {
|
is NavTarget.Messages -> {
|
||||||
val callback = object : MessagesEntryPoint.Callback {
|
val callback = object : MessagesEntryPoint.Callback {
|
||||||
override fun onRoomDetailsClicked() {
|
override fun onRoomDetailsClicked() {
|
||||||
backstack.push(NavTarget.RoomDetails)
|
backstack.push(NavTarget.RoomDetails)
|
||||||
|
|
@ -149,11 +152,18 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
|
callbacks.forEach { it.onPermalinkClicked(data) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messagesEntryPoint.createNode(this, buildContext, callback)
|
messagesEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(MessagesEntryPoint.Params(navTarget.focusedEventId))
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.RoomDetails -> {
|
NavTarget.RoomDetails -> {
|
||||||
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
||||||
|
|
@ -169,7 +179,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Messages : NavTarget
|
data class Messages(val focusedEventId: EventId? = null) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomDetails : NavTarget
|
data object RoomDetails : NavTarget
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ fun RootView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RootPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
||||||
RootView(
|
RootView(
|
||||||
state = rootState,
|
state = rootState,
|
||||||
onOpenBugReport = {},
|
onOpenBugReport = {},
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.di.RoomComponentFactory
|
import io.element.android.appnav.di.RoomComponentFactory
|
||||||
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
|
|
@ -47,14 +48,30 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
private class FakeMessagesEntryPoint : MessagesEntryPoint {
|
private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder {
|
||||||
|
var buildContext: BuildContext? = null
|
||||||
var nodeId: String? = null
|
var nodeId: String? = null
|
||||||
|
var parameters: MessagesEntryPoint.Params? = null
|
||||||
var callback: MessagesEntryPoint.Callback? = null
|
var callback: MessagesEntryPoint.Callback? = null
|
||||||
|
|
||||||
override fun createNode(parentNode: Node, buildContext: BuildContext, callback: MessagesEntryPoint.Callback): Node {
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
|
||||||
return node(buildContext) {}.also {
|
this.buildContext = buildContext
|
||||||
nodeId = it.id
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
|
||||||
|
parameters = params
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
return node(buildContext!!) {}.also {
|
||||||
|
nodeId = it.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +125,7 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
|
@ -118,9 +135,9 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages)
|
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages())
|
||||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(), Lifecycle.State.CREATED)
|
||||||
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!!
|
val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages())!!
|
||||||
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +147,7 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.appnav.intent
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.api.oidc.OidcAction
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
|
@ -26,9 +27,12 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser
|
||||||
import io.element.android.libraries.deeplink.DeepLinkCreator
|
import io.element.android.libraries.deeplink.DeepLinkCreator
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -162,9 +166,60 @@ class IntentResolverTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink`() {
|
||||||
|
val permalinkData = PermalinkData.UserLink(
|
||||||
|
userId = UserId("@alice:matrix.org")
|
||||||
|
)
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { permalinkData }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = "https://matrix.to/#/@alice:matrix.org".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
ResolvedIntent.Permalink(
|
||||||
|
permalinkData = permalinkData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink, FallbackLink should be ignored`() {
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = "https://matrix.to/#/@alice:matrix.org".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink, invalid action`() {
|
||||||
|
val permalinkData = PermalinkData.UserLink(
|
||||||
|
userId = UserId("@alice:matrix.org")
|
||||||
|
)
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { permalinkData }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
data = "https://matrix.to/invalid".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve invalid`() {
|
fun `test resolve invalid`() {
|
||||||
val sut = createIntentResolver()
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
|
||||||
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
data = "io.element:/invalid".toUri()
|
data = "io.element:/invalid".toUri()
|
||||||
|
|
@ -173,12 +228,17 @@ class IntentResolverTest {
|
||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createIntentResolver(): IntentResolver {
|
private fun createIntentResolver(
|
||||||
|
permalinkParserResult: () -> PermalinkData = { throw NotImplementedError() }
|
||||||
|
): IntentResolver {
|
||||||
return IntentResolver(
|
return IntentResolver(
|
||||||
deeplinkParser = DeeplinkParser(),
|
deeplinkParser = DeeplinkParser(),
|
||||||
oidcIntentResolver = DefaultOidcIntentResolver(
|
oidcIntentResolver = DefaultOidcIntentResolver(
|
||||||
oidcUrlParser = OidcUrlParser()
|
oidcUrlParser = OidcUrlParser()
|
||||||
),
|
),
|
||||||
|
permalinkParser = FakePermalinkParser(
|
||||||
|
result = permalinkParserResult
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AnalyticsVerificationStateMappingTests {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test verification Mappings`() = runTest {
|
||||||
|
assertThat(SessionVerifiedStatus.Verified.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.VerificationState.Verified)
|
||||||
|
assertThat(SessionVerifiedStatus.NotVerified.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.VerificationState.NotVerified)
|
||||||
|
|
||||||
|
assertThat(SessionVerifiedStatus.Verified.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.VerificationState.Verified)
|
||||||
|
assertThat(SessionVerifiedStatus.NotVerified.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.VerificationState.NotVerified)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test recovery state Mappings`() = runTest {
|
||||||
|
assertThat(RecoveryState.UNKNOWN.toAnalyticsUserPropertyValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.WAITING_FOR_SYNC.toAnalyticsUserPropertyValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.INCOMPLETE.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Incomplete)
|
||||||
|
assertThat(RecoveryState.ENABLED.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Enabled)
|
||||||
|
assertThat(RecoveryState.DISABLED.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Disabled)
|
||||||
|
|
||||||
|
assertThat(RecoveryState.UNKNOWN.toAnalyticsStateChangeValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.WAITING_FOR_SYNC.toAnalyticsStateChangeValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.INCOMPLETE.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Incomplete)
|
||||||
|
assertThat(RecoveryState.ENABLED.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Enabled)
|
||||||
|
assertThat(RecoveryState.DISABLED.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,13 +20,19 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -64,15 +70,56 @@ class LoggedInPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - report crypto status analytics`() = runTest {
|
||||||
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val verificationService = FakeSessionVerificationService()
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = LoggedInPresenter(
|
||||||
|
matrixClient = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService),
|
||||||
|
networkMonitor = FakeNetworkMonitor(NetworkStatus.Online),
|
||||||
|
pushService = FakePushService(),
|
||||||
|
sessionVerificationService = verificationService,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
encryptionService = encryptionService
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
|
verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
|
|
||||||
|
skipItems(4)
|
||||||
|
|
||||||
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedEvents[0]).isInstanceOf(CryptoSessionStateChange::class.java)
|
||||||
|
|
||||||
|
assertThat(analyticsService.capturedUserProperties.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedUserProperties[0].recoveryState).isEqualTo(UserProperties.RecoveryState.Incomplete)
|
||||||
|
assertThat(analyticsService.capturedUserProperties[0].verificationState).isEqualTo(UserProperties.VerificationState.Verified)
|
||||||
|
|
||||||
|
// ensure a sync status change does not trigger a new capture
|
||||||
|
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLoggedInPresenter(
|
private fun createLoggedInPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
networkStatus: NetworkStatus = NetworkStatus.Offline
|
networkStatus: NetworkStatus = NetworkStatus.Offline,
|
||||||
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||||
pushService = FakePushService(),
|
pushService = FakePushService(),
|
||||||
sessionVerificationService = FakeSessionVerificationService(),
|
sessionVerificationService = FakeSessionVerificationService(),
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
encryptionService = encryptionService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,9 @@ dependencyAnalysis {
|
||||||
onUnusedDependencies {
|
onUnusedDependencies {
|
||||||
exclude("com.jakewharton.timber:timber")
|
exclude("com.jakewharton.timber:timber")
|
||||||
}
|
}
|
||||||
|
onUnusedAnnotationProcessors {}
|
||||||
|
onRedundantPlugins {}
|
||||||
|
onIncorrectConfiguration {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
docs/deeplink.md
Normal file
71
docs/deeplink.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Element X Android deeplink
|
||||||
|
|
||||||
|
<!--- TOC -->
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Asset Links](#asset-links)
|
||||||
|
* [Supported links](#supported-links)
|
||||||
|
* [Developer tools](#developer-tools)
|
||||||
|
|
||||||
|
<!--- END -->
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Element X Android supports deep linking to specific screens in the application. This document explains how to use deep links in Element X Android.
|
||||||
|
|
||||||
|
### Asset Links
|
||||||
|
|
||||||
|
The asset links file is available at https://element.io/.well-known/assetlinks.json
|
||||||
|
|
||||||
|
### Supported links
|
||||||
|
|
||||||
|
Element Call link:
|
||||||
|
> https://call.element.io/Example
|
||||||
|
|
||||||
|
Link to a user:
|
||||||
|
> https://app.element.io/#/user/@alice:matrix.org
|
||||||
|
|
||||||
|
Link to a room by id or alias:
|
||||||
|
> https://app.element.io/#/room/!roomid:matrix.org
|
||||||
|
> https://app.element.io/#/room/#element-x-android:matrix.org
|
||||||
|
|
||||||
|
Link to a room with a specific event:
|
||||||
|
> https://app.element.io/#/room/!roomid:matrix.org/$eventid
|
||||||
|
|
||||||
|
Note that it will also work with other domain such as:
|
||||||
|
> https://mobile.element.io
|
||||||
|
> https://develop.element.io
|
||||||
|
> https://staging.element.io
|
||||||
|
|
||||||
|
## Developer tools
|
||||||
|
|
||||||
|
Using an Android 12 or higher emulator
|
||||||
|
|
||||||
|
Ensure links verification is enabled
|
||||||
|
```bash
|
||||||
|
adb shell am compat enable 175408749 io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset link verifications for the given package id
|
||||||
|
```bash
|
||||||
|
adb shell pm set-app-links --package io.element.android.x.debug 0 all
|
||||||
|
```
|
||||||
|
|
||||||
|
Force the package id links to be verified
|
||||||
|
```bash
|
||||||
|
adb shell pm verify-app-links --re-verify io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the link verification of the package id
|
||||||
|
```bash
|
||||||
|
adb shell pm get-app-links io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
io.element.android.x.debug:
|
||||||
|
ID: e2ece472-c266-4bf0-829c-be79959a6270
|
||||||
|
Signatures: [B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E]
|
||||||
|
Domain verification state:
|
||||||
|
*.element.io: 1024
|
||||||
|
```
|
||||||
10
fastlane/metadata/android/en-US/changelogs/40004120.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/40004120.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Main changes in this version:
|
||||||
|
|
||||||
|
- Added support for opening matrix URLs inside the app and navigating to replied to messages.
|
||||||
|
- Added per-app language support for Android 13+.
|
||||||
|
- Session verification is no longer mandatory for already logged in users.
|
||||||
|
- Better log handling.
|
||||||
|
- Fixed CVE-2024-34353 / GHSA-9ggc-845v-gcgv.
|
||||||
|
- UX improvements.
|
||||||
|
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s ."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
|
||||||
import io.element.android.libraries.designsystem.components.PageTitle
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s ."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,12 @@
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.microphone"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
@ -28,24 +32,27 @@
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ElementCallActivity"
|
android:name=".ui.ElementCallActivity"
|
||||||
android:label="@string/element_call"
|
|
||||||
android:exported="true"
|
|
||||||
android:taskAffinity="io.element.android.features.call"
|
|
||||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
android:launchMode="singleTask">
|
android:exported="true"
|
||||||
|
android:label="@string/element_call"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="io.element.android.features.call">
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Matching asset file: https://call.element.io/.well-known/assetlinks.json -->
|
||||||
<data android:host="call.element.io" />
|
<data android:host="call.element.io" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
|
@ -55,6 +62,7 @@
|
||||||
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
|
@ -62,7 +70,10 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
|
<service
|
||||||
|
android:name=".CallForegroundService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -28,6 +27,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||||
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string>
|
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
|
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Chat sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
|
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
|
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,12 @@ package io.element.android.features.ftue.api
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
interface FtueEntryPoint : FeatureEntryPoint {
|
interface FtueEntryPoint : FeatureEntryPoint {
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
fun callback(callback: Callback): NodeBuilder
|
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
|
||||||
fun onFtueFlowFinished()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
|
@ -60,6 +61,7 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.permissions.impl)
|
testImplementation(projects.libraries.permissions.impl)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,6 @@ class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint {
|
||||||
val plugins = ArrayList<Plugin>()
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
return object : FtueEntryPoint.NodeBuilder {
|
return object : FtueEntryPoint.NodeBuilder {
|
||||||
override fun callback(callback: FtueEntryPoint.Callback): FtueEntryPoint.NodeBuilder {
|
|
||||||
plugins += callback
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun build(): Node {
|
override fun build(): Node {
|
||||||
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
|
||||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||||
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
|
|
@ -86,8 +85,6 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
data object LockScreenSetup : NavTarget
|
data object LockScreenSetup : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
||||||
|
|
@ -143,7 +140,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveToNextStep() {
|
private fun moveToNextStep() = lifecycleScope.launch {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
FtueStep.SessionVerification -> {
|
FtueStep.SessionVerification -> {
|
||||||
backstack.newRoot(NavTarget.SessionVerification)
|
backstack.newRoot(NavTarget.SessionVerification)
|
||||||
|
|
@ -157,7 +154,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
FtueStep.LockscreenSetup -> {
|
FtueStep.LockscreenSetup -> {
|
||||||
backstack.newRoot(NavTarget.LockScreenSetup)
|
backstack.newRoot(NavTarget.LockScreenSetup)
|
||||||
}
|
}
|
||||||
null -> callback?.onFtueFlowFinished()
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
|
||||||
import io.element.android.libraries.designsystem.components.PageTitle
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object CreateNewRecoveryKey : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
|
|
@ -68,10 +65,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
|
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
|
||||||
override fun onCreateNewRecoveryKey() {
|
|
||||||
backstack.push(NavTarget.CreateNewRecoveryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Move to the completed state view in the verification flow
|
// Move to the completed state view in the verification flow
|
||||||
|
|
@ -89,10 +82,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.EnterRecoveryKey)
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateNewRecoveryKey() {
|
|
||||||
backstack.push(NavTarget.CreateNewRecoveryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
plugins<Callback>().forEach { it.onDone() }
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
|
|
@ -105,12 +94,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
.callback(secureBackupEntryPointCallback)
|
.callback(secureBackupEntryPointCallback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
is NavTarget.CreateNewRecoveryKey -> {
|
|
||||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey))
|
|
||||||
.callback(secureBackupEntryPointCallback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,26 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.timeout
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueService @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
|
|
@ -44,6 +52,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : FtueService {
|
) : FtueService {
|
||||||
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||||
|
|
||||||
|
|
@ -55,7 +64,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sessionVerificationService.needsVerificationFlow
|
sessionVerificationService.sessionVerifiedStatus
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
|
|
@ -64,7 +73,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (isSessionNotVerified()) {
|
null -> if (isSessionNotVerified()) {
|
||||||
FtueStep.SessionVerification
|
FtueStep.SessionVerification
|
||||||
|
|
@ -89,8 +98,8 @@ class DefaultFtueService @Inject constructor(
|
||||||
FtueStep.AnalyticsOptIn -> null
|
FtueStep.AnalyticsOptIn -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private suspend fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf<suspend () -> Boolean>(
|
||||||
{ isSessionNotVerified() },
|
{ isSessionNotVerified() },
|
||||||
{ shouldAskNotificationPermissions() },
|
{ shouldAskNotificationPermissions() },
|
||||||
{ needsAnalyticsOptIn() },
|
{ needsAnalyticsOptIn() },
|
||||||
|
|
@ -98,16 +107,28 @@ class DefaultFtueService @Inject constructor(
|
||||||
).any { it() }
|
).any { it() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSessionNotVerified(): Boolean {
|
@OptIn(FlowPreview::class)
|
||||||
return sessionVerificationService.needsVerificationFlow.value
|
private suspend fun isSessionNotVerified(): Boolean {
|
||||||
|
// Wait for the first known (or ready) verification status
|
||||||
|
val readyVerifiedSessionStatus = sessionVerificationService.sessionVerifiedStatus
|
||||||
|
.filter { it != SessionVerifiedStatus.Unknown }
|
||||||
|
// This is not ideal, but there are some very rare cases when reading the flow seems to get stuck
|
||||||
|
.timeout(5.seconds)
|
||||||
|
.catch {
|
||||||
|
Timber.e(it, "Failed to get session verification status, assume it's not verified")
|
||||||
|
emit(SessionVerifiedStatus.NotVerified)
|
||||||
|
}
|
||||||
|
.first()
|
||||||
|
val skipVerification = suspend { sessionPreferencesStore.isSessionVerificationSkipped().first() }
|
||||||
|
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return analyticsService.didAskUserConsent().first().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldAskNotificationPermissions(): Boolean {
|
private suspend fun shouldAskNotificationPermissions(): Boolean {
|
||||||
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||||
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
||||||
|
|
@ -118,14 +139,12 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldDisplayLockscreenSetup(): Boolean {
|
private suspend fun shouldDisplayLockscreenSetup(): Boolean {
|
||||||
return runBlocking {
|
return lockScreenService.isSetupRequired().first()
|
||||||
lockScreenService.isSetupRequired().first()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun updateState() {
|
internal suspend fun updateState() {
|
||||||
state.value = when {
|
state.value = when {
|
||||||
isAnyStepIncomplete() -> FtueState.Incomplete
|
isAnyStepIncomplete() -> FtueState.Incomplete
|
||||||
else -> FtueState.Complete
|
else -> FtueState.Complete
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,26 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
|
||||||
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Устанаўленне злучэння"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Ўсталяванне бяспечнага злучэння"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Паспрабуйце зноў увайсці ў сістэму з дапамогай QR-кода, калі гэта была сеткавая праблема"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Паспрабуйце зноў увайсці ў сістэму з дапамогай QR-кода, калі гэта была сеткавая праблема"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Калі гэта не дапамагло, увайдзіце ўручную"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Калі гэта не дапамагло, увайдзіце ўручную"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя ніжэй."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Увядзіце нумар на прыладзе"</string>
|
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Выберыце %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Выконвайце паказаныя інструкцыі"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Паказаць QR-код”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць Element выкарыстоўваць камеру вашай прылады."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць %1$s выкарыстоўваць камеру вашай прылады."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
|
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
|
||||||
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
|
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Navazování spojení"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Navazování zabezpečeného spojení"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Zkuste se znovu přihlásit pomocí QR kódu v případě, že se jednalo o problém se sítí"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Zkuste se znovu přihlásit pomocí QR kódu v případě, že se jednalo o problém se sítí"</string>
|
||||||
|
|
@ -10,19 +10,18 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Pokud to nefunguje, přihlaste se ručně"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Pokud to nefunguje, přihlaste se ručně"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Zadejte číslo na svém zařízení"</string>
|
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Vybrat %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Zobrazit QR kód\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci Element udělit povolení k použití kamery vašeho zařízení."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci %1$s udělit povolení k použití kamery vašeho zařízení."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,33 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
|
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
|
||||||
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Verbindung aufbauen"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Sichere Verbindung aufbauen"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Versuche, dich erneut mit einem QR-Code anzumelden, falls dies ein Netzwerkproblem war."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Versuche, dich erneut mit einem QR-Code anzumelden, falls dies ein Netzwerkproblem war."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst Element die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Warten auf dein anderes Gerät"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Dein Account-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Dein Verifizierungscode"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,33 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
|
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
|
||||||
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
|
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Établissement de la connexion"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Établissement d’une connexion sécurisée"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Essayez de vous connecter à nouveau à l’aide du code QR au cas où il s’agirait d’un problème réseau"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Si vous rencontrez le même problème, essayez un autre réseau wifi ou utilisez vos données mobiles au lieu du wifi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Si cela ne fonctionne pas, connectez-vous manuellement"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion n’est pas sécurisée"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Choisissez %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Accéder aux paramètres de l’appareil photo"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Vous devez autoriser %1$s à utiliser la camera de votre appareil pour continuer."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Autoriser l’usage de la caméra pour scanner le code QR"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Recommencer"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Une erreur inattendue s’est produite. Veuillez réessayer."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"En attente de votre autre session"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Votre fournisseur de compte peut vous demander le code suivant pour vérifier la connexion."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Votre code de vérification"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
||||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||||
<string name="screen_welcome_bullet_3">"N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."</string>
|
<string name="screen_welcome_bullet_3">"N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,33 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
|
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
|
||||||
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
|
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Kapcsolat létesítése"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Biztonságos kapcsolat létesítése"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Válassza ezt: %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"„QR-kód megjelenítése”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az Element használhassa az eszköz kameráját."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az %1$s használhassa az eszköz kameráját."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Várakozás a másik eszközre"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"A fiókszolgáltatója kérheti a következő kódot a bejelentkezése ellenőrzéséhez."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Az Ön ellenőrzőkódja"</string>
|
||||||
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
||||||
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,31 @@
|
||||||
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
||||||
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jika tidak berhasil, masuk secara manual"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Koneksi tidak aman"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Anda akan diminta untuk memasukkan dua digit yang ditunjukkan di bawah ini."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Masukkan nomor di perangkat Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Pergi ke pengaturan kamera"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Anda perlu memberikan izin ke %1$s untuk menggunakan kamera perangkat Anda untuk melanjutkan."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Izinkan akses kamera untuk memindai kode QR"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Mulai dari awal"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Terjadi kesalahan tak terduga. Silakan coba lagi."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Menunggu perangkat Anda yang lain"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Penyedia akun Anda mungkin meminta kode berikut untuk memverifikasi proses masuk."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Kode verifikasi Anda"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,25 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам будет предложено ввести две цифры, показанные ниже."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Введите номер на своем устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить Element использовать камеру вашего устройства."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить %1$s использовать камеру вашего устройства."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"В ожидании другого устройства"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Поставщик учетной записи может запросить следующий код для подтверждения входа."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Ваш код подтверждения"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,26 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
|
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
|
||||||
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
|
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie spojenia"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie bezpečného spojenia"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ak to nefunguje, prihláste sa manuálne"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ak to nefunguje, prihláste sa manuálne"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete vyzvaní na zadanie dvoch číslic uvedených nižšie."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Zadajte číslo na svojom zariadení"</string>
|
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podľa zobrazených pokynov"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"„Zobraziť QR kód“"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii Element používať fotoaparát vášho zariadenia."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii %1$s používať fotoaparát vášho zariadenia."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Du kan ändra dina inställningar senare."</string>
|
<string name="screen_notification_optin_subtitle">"Du kan ändra dina inställningar senare."</string>
|
||||||
<string name="screen_notification_optin_title">"Tillåt aviseringar och missa aldrig ett meddelande"</string>
|
<string name="screen_notification_optin_title">"Tillåt aviseringar och missa aldrig ett meddelande"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Samtal, omröstningar, sökning och mer kommer att läggas till senare i år."</string>
|
<string name="screen_welcome_bullet_1">"Samtal, omröstningar, sökning och mer kommer att läggas till senare i år."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Meddelandehistorik för krypterade rum är inte tillgänglig än."</string>
|
<string name="screen_welcome_bullet_2">"Meddelandehistorik för krypterade rum är inte tillgänglig än."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Vi vill gärna höra från dig, låt oss veta vad du tycker via inställningssidan."</string>
|
<string name="screen_welcome_bullet_3">"Vi vill gärna höra från dig, låt oss veta vad du tycker via inställningssidan."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,26 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
||||||
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Establishing connection"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Establishing a secure connection"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Try signing in again with a QR code in case this was a network problem"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Try signing in again with a QR code in case this was a network problem"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown below."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Enter number on your device"</string>
|
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Follow the instructions shown"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for Element to use your device’s camera in order to continue."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your device’s camera in order to continue."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Start over"</string>
|
<string name="screen_qr_code_login_start_over_button">"Start over"</string>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
|
|
@ -90,7 +91,6 @@ class DefaultFtueServiceTests {
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
val sessionVerificationService = FakeSessionVerificationService().apply {
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
givenNeedsVerification(true)
|
|
||||||
}
|
}
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
|
|
@ -108,7 +108,7 @@ class DefaultFtueServiceTests {
|
||||||
|
|
||||||
// Session verification
|
// Session verification
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
sessionVerificationService.givenNeedsVerification(false)
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
|
||||||
// Notifications opt in
|
// Notifications opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
@ -200,6 +200,7 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueService(
|
) = DefaultFtueService(
|
||||||
|
|
@ -209,5 +210,6 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.features.invite.test)
|
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.invite.impl.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.invite.impl.R
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
private val minHeight = 72.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun InviteSummaryRow(
|
|
||||||
invite: InviteListInviteSummary,
|
|
||||||
onAcceptClicked: () -> Unit,
|
|
||||||
onDeclineClicked: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = minHeight)
|
|
||||||
) {
|
|
||||||
DefaultInviteSummaryRow(
|
|
||||||
invite = invite,
|
|
||||||
onAcceptClicked = onAcceptClicked,
|
|
||||||
onDeclineClicked = onDeclineClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DefaultInviteSummaryRow(
|
|
||||||
invite: InviteListInviteSummary,
|
|
||||||
onAcceptClicked: () -> Unit,
|
|
||||||
onDeclineClicked: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.height(IntrinsicSize.Min),
|
|
||||||
verticalAlignment = Alignment.Top
|
|
||||||
) {
|
|
||||||
Avatar(
|
|
||||||
invite.roomAvatarData,
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 16.dp, end = 4.dp)
|
|
||||||
.alignByBaseline()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
|
||||||
|
|
||||||
// Name
|
|
||||||
Text(
|
|
||||||
text = invite.roomName,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
|
||||||
modifier = Modifier.padding(end = bonusPadding),
|
|
||||||
)
|
|
||||||
|
|
||||||
// ID or Alias
|
|
||||||
invite.roomAlias?.let {
|
|
||||||
Text(
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
text = it,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.padding(end = bonusPadding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender
|
|
||||||
invite.sender?.let { sender ->
|
|
||||||
SenderRow(sender = sender)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CTAs
|
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
|
||||||
OutlinedButton(
|
|
||||||
text = stringResource(CommonStrings.action_decline),
|
|
||||||
onClick = onDeclineClicked,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
text = stringResource(CommonStrings.action_accept),
|
|
||||||
onClick = onAcceptClicked,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
size = ButtonSize.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UnreadIndicatorAtom(isVisible = invite.isNew)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SenderRow(sender: InviteSender) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier.padding(top = 6.dp),
|
|
||||||
) {
|
|
||||||
Avatar(
|
|
||||||
avatarData = sender.avatarData,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
|
||||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
|
||||||
AnnotatedString(
|
|
||||||
text = text,
|
|
||||||
spanStyles = listOf(
|
|
||||||
AnnotatedString.Range(
|
|
||||||
SpanStyle(
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
|
||||||
start = senderNameStart,
|
|
||||||
end = senderNameStart + sender.displayName.length
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun InviteSummaryRowPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) = ElementPreview {
|
|
||||||
InviteSummaryRow(
|
|
||||||
invite = data,
|
|
||||||
onAcceptClicked = {},
|
|
||||||
onDeclineClicked = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class InviteListPresenter @Inject constructor(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
private val store: SeenInvitesStore,
|
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
|
||||||
) : Presenter<InviteListState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): InviteListState {
|
|
||||||
val invites by client
|
|
||||||
.roomListService
|
|
||||||
.invites
|
|
||||||
.summaries
|
|
||||||
.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
seenInvites = store.seenRoomIds().first()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(invites) {
|
|
||||||
store.markAsSeen(
|
|
||||||
invites
|
|
||||||
.filterIsInstance<RoomSummary.Filled>()
|
|
||||||
.map { it.details.roomId }
|
|
||||||
.toSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
|
||||||
|
|
||||||
fun handleEvent(event: InviteListEvents) {
|
|
||||||
when (event) {
|
|
||||||
is InviteListEvents.AcceptInvite -> {
|
|
||||||
acceptDeclineInviteState.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is InviteListEvents.DeclineInvite -> {
|
|
||||||
acceptDeclineInviteState.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val inviteList = remember(seenInvites, invites) {
|
|
||||||
invites
|
|
||||||
.filterIsInstance<RoomSummary.Filled>()
|
|
||||||
.map {
|
|
||||||
it.toInviteSummary(seenInvites.contains(it.details.roomId))
|
|
||||||
}
|
|
||||||
.toPersistentList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return InviteListState(
|
|
||||||
inviteList = inviteList,
|
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
|
||||||
eventSink = ::handleEvent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
|
||||||
val i = inviter
|
|
||||||
val avatarData = if (isDirect && i != null) {
|
|
||||||
AvatarData(
|
|
||||||
id = i.userId.value,
|
|
||||||
name = i.displayName,
|
|
||||||
url = i.avatarUrl,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AvatarData(
|
|
||||||
id = roomId.value,
|
|
||||||
name = name,
|
|
||||||
url = avatarUrl,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val alias = if (isDirect) {
|
|
||||||
inviter?.userId?.value
|
|
||||||
} else {
|
|
||||||
canonicalAlias
|
|
||||||
}
|
|
||||||
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = name,
|
|
||||||
roomAlias = alias,
|
|
||||||
roomAvatarData = avatarData,
|
|
||||||
isDirect = isDirect,
|
|
||||||
isNew = !seen,
|
|
||||||
sender = inviter
|
|
||||||
?.takeIf { !isDirect }
|
|
||||||
?.run {
|
|
||||||
InviteSender(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName ?: "",
|
|
||||||
avatarData = AvatarData(
|
|
||||||
id = userId.value,
|
|
||||||
name = displayName,
|
|
||||||
url = avatarUrl,
|
|
||||||
size = AvatarSize.InviteSender,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun InviteListInviteSummary.toInviteData() = InviteData(
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = roomName,
|
|
||||||
isDirect = isDirect,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
|
|
||||||
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
|
||||||
private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider()
|
|
||||||
|
|
||||||
override val values: Sequence<InviteListState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
anInviteListState(),
|
|
||||||
anInviteListState(inviteList = persistentListOf()),
|
|
||||||
) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState ->
|
|
||||||
anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun anInviteListState(
|
|
||||||
inviteList: ImmutableList<InviteListInviteSummary> = aInviteListInviteSummaryList(),
|
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
|
||||||
eventSink: (InviteListEvents) -> Unit = {}
|
|
||||||
) = InviteListState(
|
|
||||||
inviteList = inviteList,
|
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
|
||||||
return persistentListOf(
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id1:example.com"),
|
|
||||||
roomName = "Room 1",
|
|
||||||
roomAlias = "#room:example.org",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@alice:example.org"),
|
|
||||||
displayName = "Alice"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id2:example.com"),
|
|
||||||
roomName = "Room 2",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@bob:example.org"),
|
|
||||||
displayName = "Bob"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!id3:example.com"),
|
|
||||||
roomName = "Alice",
|
|
||||||
roomAlias = "@alice:example.com"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.invite.impl.invitelist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.invite.impl.R
|
|
||||||
import io.element.android.features.invite.impl.components.InviteSummaryRow
|
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InviteListView(
|
|
||||||
state: InviteListState,
|
|
||||||
onBackClicked: () -> Unit,
|
|
||||||
onInviteAccepted: (RoomId) -> Unit,
|
|
||||||
onInviteDeclined: (RoomId) -> Unit,
|
|
||||||
onInviteClicked: (RoomId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
InviteListContent(
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
onInviteClicked = onInviteClicked,
|
|
||||||
onBackClicked = onBackClicked,
|
|
||||||
)
|
|
||||||
AcceptDeclineInviteView(
|
|
||||||
state = state.acceptDeclineInviteState,
|
|
||||||
onInviteAccepted = onInviteAccepted,
|
|
||||||
onInviteDeclined = onInviteDeclined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun InviteListContent(
|
|
||||||
state: InviteListState,
|
|
||||||
onBackClicked: () -> Unit,
|
|
||||||
onInviteClicked: (RoomId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = onBackClicked)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(CommonStrings.action_invites_list),
|
|
||||||
style = ElementTheme.typography.aliasScreenTitle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
content = { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
) {
|
|
||||||
if (state.inviteList.isEmpty()) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_invites_empty_list),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = state.inviteList,
|
|
||||||
) { index, invite ->
|
|
||||||
InviteSummaryRow(
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
onClick = { onInviteClicked(invite.roomId) }
|
|
||||||
),
|
|
||||||
invite = invite,
|
|
||||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
|
||||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (index != state.inviteList.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = ElementPreview {
|
|
||||||
InviteListView(
|
|
||||||
state = state,
|
|
||||||
onBackClicked = {},
|
|
||||||
onInviteAccepted = {},
|
|
||||||
onInviteDeclined = {},
|
|
||||||
onInviteClicked = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.invite.impl.model
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class InviteListInviteSummary(
|
|
||||||
val roomId: RoomId,
|
|
||||||
val roomName: String = "",
|
|
||||||
val roomAlias: String? = null,
|
|
||||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName, size = AvatarSize.RoomInviteItem),
|
|
||||||
val sender: InviteSender? = null,
|
|
||||||
val isDirect: Boolean = false,
|
|
||||||
val isNew: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class InviteSender(
|
|
||||||
val userId: UserId,
|
|
||||||
val displayName: String,
|
|
||||||
val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
|
||||||
)
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.invite.impl.model
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteListInviteSummary> {
|
|
||||||
override val values: Sequence<InviteListInviteSummary>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aInviteListInviteSummary(),
|
|
||||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com"),
|
|
||||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com", isNew = true),
|
|
||||||
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
|
||||||
aInviteListInviteSummary().copy(isNew = true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aInviteListInviteSummary() = InviteListInviteSummary(
|
|
||||||
roomId = RoomId("!room1:example.com"),
|
|
||||||
roomName = "Some room with a long name that will truncate",
|
|
||||||
sender = InviteSender(
|
|
||||||
userId = UserId("@alice-with-a-long-mxid:example.org"),
|
|
||||||
displayName = "Alice with a long name"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
@ -102,12 +102,14 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
acceptedAction.runUpdatingState {
|
acceptedAction.runUpdatingState {
|
||||||
client.joinRoom(roomId).onSuccess {
|
client.joinRoom(roomId)
|
||||||
|
.onSuccess {
|
||||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||||
client.getRoom(roomId)?.use { room ->
|
client.getRoom(roomId)?.use { room ->
|
||||||
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.map { roomId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
|
|
@ -29,6 +28,7 @@ import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
@ -102,9 +102,9 @@ private fun DeclineConfirmationDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AcceptDeclineInviteViewLightPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
|
internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.invite.impl.invitelist
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.TurbineTestContext
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.test.FakeSeenInvitesStore
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class InviteListPresenterTests {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - starts empty, adds invites when received`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.inviteList).isEmpty()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withDirectChatInvitation()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.RoomInviteItem,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - includes sender details for room invites`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().withRoomInvitation()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(roomListService = roomListService)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val withInviteState = awaitInitialItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
|
|
||||||
AvatarData(
|
|
||||||
id = A_USER_ID.value,
|
|
||||||
name = A_USER_NAME,
|
|
||||||
url = AN_AVATAR_URL,
|
|
||||||
size = AvatarSize.InviteSender,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - stores seen invites when received`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val store = FakeSeenInvitesStore()
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
// When one invite is received, that ID is saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
|
||||||
|
|
||||||
// When a second is added, both are saved
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
|
||||||
|
|
||||||
// When they're both dismissed, an empty set is saved
|
|
||||||
roomListService.postInviteRooms(listOf())
|
|
||||||
|
|
||||||
awaitItem()
|
|
||||||
assertThat(store.getProvidedRoomIds()).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - marks invite as new if they're unseen`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val store = FakeSeenInvitesStore()
|
|
||||||
store.publishRoomIds(setOf(A_ROOM_ID))
|
|
||||||
val presenter = createInviteListPresenter(
|
|
||||||
FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
),
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
|
||||||
assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
|
||||||
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
|
||||||
assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
|
|
||||||
postInviteRooms(
|
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = true,
|
|
||||||
lastMessage = null,
|
|
||||||
inviter = aRoomMember(
|
|
||||||
userId = A_USER_ID,
|
|
||||||
displayName = A_USER_NAME,
|
|
||||||
avatarUrl = AN_AVATAR_URL,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
|
||||||
aRoomSummaryDetails(
|
|
||||||
roomId = id,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarUrl = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
|
||||||
skipItems(1)
|
|
||||||
return awaitItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInviteListPresenter(
|
|
||||||
client: MatrixClient,
|
|
||||||
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
|
||||||
) = InviteListPresenter(
|
|
||||||
client,
|
|
||||||
seenInvitesStore,
|
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -164,7 +164,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - accepting invite error flow`() = runTest {
|
fun `present - accepting invite error flow`() = runTest {
|
||||||
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
|
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||||
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId"))
|
Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
joinRoomLambda = joinRoomFailure
|
joinRoomLambda = joinRoomFailure
|
||||||
|
|
@ -197,8 +197,8 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - accepting invite success flow`() = runTest {
|
fun `present - accepting invite success flow`() = runTest {
|
||||||
val joinRoomSuccess = lambdaRecorder { roomId: RoomId ->
|
val joinRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||||
Result.success(roomId)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
joinRoomLambda = joinRoomSuccess
|
joinRoomLambda = joinRoomSuccess
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.invite.test
|
|
||||||
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
|
|
||||||
class FakeSeenInvitesStore : SeenInvitesStore {
|
|
||||||
private val existing = MutableStateFlow(emptySet<RoomId>())
|
|
||||||
private var provided: Set<RoomId>? = null
|
|
||||||
|
|
||||||
fun publishRoomIds(invites: Set<RoomId>) {
|
|
||||||
existing.value = invites
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProvidedRoomIds() = provided
|
|
||||||
|
|
||||||
override fun seenRoomIds(): Flow<Set<RoomId>> = existing
|
|
||||||
|
|
||||||
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
|
||||||
provided = roomIds.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
interface JoinRoomEntryPoint : FeatureEntryPoint {
|
interface JoinRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
|
@ -29,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: Optional<RoomDescription>,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.joinroom.impl"
|
namespace = "io.element.android.features.joinroom.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -46,11 +51,13 @@ dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.invite.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
sealed interface JoinRoomEvents {
|
sealed interface JoinRoomEvents {
|
||||||
|
data object RetryFetchingContent : JoinRoomEvents
|
||||||
data object JoinRoom : JoinRoomEvents
|
data object JoinRoom : JoinRoomEvents
|
||||||
|
data object KnockRoom : JoinRoomEvents
|
||||||
|
data object ClearError : JoinRoomEvents
|
||||||
data object AcceptInvite : JoinRoomEvents
|
data object AcceptInvite : JoinRoomEvents
|
||||||
data object DeclineInvite : JoinRoomEvents
|
data object DeclineInvite : JoinRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,11 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
||||||
private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription)
|
private val presenter = presenterFactory.create(
|
||||||
|
inputs.roomId,
|
||||||
|
inputs.roomIdOrAlias,
|
||||||
|
inputs.roomDescription,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
@ -45,6 +49,7 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
|
onKnockSuccess = ::navigateUp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
acceptDeclineInviteView.Render(
|
acceptDeclineInviteView.Render(
|
||||||
|
|
|
||||||
|
|
@ -16,47 +16,91 @@
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import org.jetbrains.annotations.VisibleForTesting
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
class JoinRoomPresenter @AssistedInject constructor(
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomId: RoomId,
|
@Assisted private val roomId: RoomId,
|
||||||
|
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||||
@Assisted private val roomDescription: Optional<RoomDescription>,
|
@Assisted private val roomDescription: Optional<RoomDescription>,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val knockRoom: KnockRoom,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
|
fun create(
|
||||||
|
roomId: RoomId,
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
roomDescription: Optional<RoomDescription>,
|
||||||
|
): JoinRoomPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): JoinRoomState {
|
override fun present(): JoinRoomState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
||||||
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) {
|
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
value = when {
|
val contentState by produceState<ContentState>(
|
||||||
|
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||||
|
key1 = roomInfo,
|
||||||
|
key2 = retryCount,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
roomInfo.isPresent -> {
|
roomInfo.isPresent -> {
|
||||||
roomInfo.get().toContentState()
|
value = roomInfo.get().toContentState()
|
||||||
}
|
}
|
||||||
roomDescription.isPresent -> {
|
roomDescription.isPresent -> {
|
||||||
roomDescription.get().toContentState()
|
value = roomDescription.get().toContentState()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ContentState.Loading(roomId)
|
value = ContentState.Loading(roomIdOrAlias)
|
||||||
|
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
|
||||||
|
value = result.fold(
|
||||||
|
onSuccess = { roomPreview ->
|
||||||
|
roomPreview.toContentState()
|
||||||
|
},
|
||||||
|
onFailure = { throwable ->
|
||||||
|
if (throwable.message?.contains("403") == true) {
|
||||||
|
ContentState.UnknownRoom(roomIdOrAlias)
|
||||||
|
} else {
|
||||||
|
ContentState.Failure(roomIdOrAlias, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,27 +108,65 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
fun handleEvents(event: JoinRoomEvents) {
|
fun handleEvents(event: JoinRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
|
JoinRoomEvents.AcceptInvite,
|
||||||
|
JoinRoomEvents.JoinRoom -> {
|
||||||
val inviteData = contentState.toInviteData() ?: return
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
JoinRoomEvents.KnockRoom -> {
|
||||||
|
coroutineScope.knockRoom(roomId, knockAction)
|
||||||
|
}
|
||||||
JoinRoomEvents.DeclineInvite -> {
|
JoinRoomEvents.DeclineInvite -> {
|
||||||
val inviteData = contentState.toInviteData() ?: return
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
JoinRoomEvents.RetryFetchingContent -> {
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
JoinRoomEvents.ClearError -> {
|
||||||
|
knockAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JoinRoomState(
|
return JoinRoomState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
knockAction = knockAction.value,
|
||||||
|
applicationName = buildMeta.applicationName,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.knockRoom(roomId: RoomId, knockAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
knockAction.runUpdatingState {
|
||||||
|
knockRoom(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomPreview.toContentState(): ContentState {
|
||||||
|
return ContentState.Loaded(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
topic = topic,
|
||||||
|
alias = canonicalAlias,
|
||||||
|
numberOfMembers = numberOfJoinedMembers,
|
||||||
|
isDirect = false,
|
||||||
|
roomType = roomType,
|
||||||
|
roomAvatarUrl = avatarUrl,
|
||||||
|
joinAuthorisationStatus = when {
|
||||||
|
// Note when isInvited, roomInfo will be used, so if this happen, it will be temporary.
|
||||||
|
isInvited -> JoinAuthorisationStatus.IsInvited(null)
|
||||||
|
canKnock -> JoinAuthorisationStatus.CanKnock
|
||||||
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
@ -96,6 +178,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
alias = alias,
|
alias = alias,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (joinRule) {
|
joinAuthorisationStatus = when (joinRule) {
|
||||||
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
|
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
|
||||||
|
|
@ -108,15 +191,18 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun MatrixRoomInfo.toContentState(): ContentState {
|
internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
return ContentState.Loaded(
|
return ContentState.Loaded(
|
||||||
roomId = RoomId(id),
|
roomId = id,
|
||||||
name = name,
|
name = name,
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = activeMembersCount,
|
numberOfMembers = activeMembersCount,
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
|
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when {
|
joinAuthorisationStatus = when {
|
||||||
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited
|
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteSender = inviter?.toInviteSender()
|
||||||
|
)
|
||||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +214,8 @@ internal fun ContentState.toInviteData(): InviteData? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ContentState.Loaded -> InviteData(
|
is ContentState.Loaded -> InviteData(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomName = computedTitle,
|
// Note: name should not be null at this point, but use Id just in case...
|
||||||
|
roomName = name ?: roomId.value,
|
||||||
isDirect = isDirect
|
isDirect = isDirect
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,21 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class JoinRoomState(
|
data class JoinRoomState(
|
||||||
val contentState: ContentState,
|
val contentState: ContentState,
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
|
val knockAction: AsyncAction<Unit>,
|
||||||
|
val applicationName: String,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val joinAuthorisationStatus = when (contentState) {
|
val joinAuthorisationStatus = when (contentState) {
|
||||||
|
|
@ -35,26 +42,20 @@ data class JoinRoomState(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ContentState {
|
sealed interface ContentState {
|
||||||
data class Loading(val roomId: RoomId) : ContentState
|
data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||||
data class UnknownRoom(val roomId: RoomId) : ContentState
|
data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState
|
||||||
|
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val topic: String?,
|
val topic: String?,
|
||||||
val alias: String?,
|
val alias: RoomAlias?,
|
||||||
val numberOfMembers: Long?,
|
val numberOfMembers: Long?,
|
||||||
val isDirect: Boolean,
|
val isDirect: Boolean,
|
||||||
|
val roomType: RoomType,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||||
) : ContentState {
|
) : ContentState {
|
||||||
val computedTitle = name ?: roomId.value
|
|
||||||
|
|
||||||
val computedSubtitle = when {
|
|
||||||
alias != null -> alias
|
|
||||||
name == null -> ""
|
|
||||||
else -> roomId.value
|
|
||||||
}
|
|
||||||
|
|
||||||
val showMemberCount = numberOfMembers != null
|
val showMemberCount = numberOfMembers != null
|
||||||
|
|
||||||
fun avatarData(size: AvatarSize): AvatarData {
|
fun avatarData(size: AvatarSize): AvatarData {
|
||||||
|
|
@ -68,9 +69,9 @@ sealed interface ContentState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class JoinAuthorisationStatus {
|
sealed interface JoinAuthorisationStatus {
|
||||||
IsInvited,
|
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||||
CanKnock,
|
data object CanKnock : JoinAuthorisationStatus
|
||||||
CanJoin,
|
data object CanJoin : JoinAuthorisationStatus
|
||||||
Unknown,
|
data object Unknown : JoinAuthorisationStatus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,16 @@ package io.element.android.features.joinroom.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
override val values: Sequence<JoinRoomState>
|
override val values: Sequence<JoinRoomState>
|
||||||
|
|
@ -30,29 +39,75 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = anUnknownContentState()
|
contentState = anUnknownContentState()
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
name = null,
|
||||||
|
alias = null,
|
||||||
|
topic = null,
|
||||||
|
)
|
||||||
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock)
|
contentState = aLoadedContentState(
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
|
||||||
|
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
|
||||||
|
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
|
||||||
|
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
|
||||||
|
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
|
||||||
|
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
|
||||||
|
numberOfMembers = 888,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
numberOfMembers = 123,
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState()
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
roomId = RoomId("!aSpaceId:domain"),
|
||||||
|
name = "A space",
|
||||||
|
alias = null,
|
||||||
|
topic = "This is the topic of a space",
|
||||||
|
roomType = RoomType.Space,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId)
|
fun aFailureContentState(
|
||||||
|
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias()
|
||||||
|
): ContentState {
|
||||||
|
return ContentState.Failure(
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
error = Exception("Error"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId)
|
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
|
||||||
|
|
||||||
|
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
|
||||||
|
|
||||||
fun aLoadedContentState(
|
fun aLoadedContentState(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = "Element X android",
|
name: String? = "Element X android",
|
||||||
alias: String? = "#exa:matrix.org",
|
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
||||||
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
||||||
numberOfMembers: Long? = null,
|
numberOfMembers: Long? = null,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
|
roomType: RoomType = RoomType.Room,
|
||||||
roomAvatarUrl: String? = null,
|
roomAvatarUrl: String? = null,
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
||||||
) = ContentState.Loaded(
|
) = ContentState.Loaded(
|
||||||
|
|
@ -62,6 +117,7 @@ fun aLoadedContentState(
|
||||||
topic = topic,
|
topic = topic,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
|
roomType = roomType,
|
||||||
roomAvatarUrl = roomAvatarUrl,
|
roomAvatarUrl = roomAvatarUrl,
|
||||||
joinAuthorisationStatus = joinAuthorisationStatus
|
joinAuthorisationStatus = joinAuthorisationStatus
|
||||||
)
|
)
|
||||||
|
|
@ -69,11 +125,25 @@ fun aLoadedContentState(
|
||||||
fun aJoinRoomState(
|
fun aJoinRoomState(
|
||||||
contentState: ContentState = aLoadedContentState(),
|
contentState: ContentState = aLoadedContentState(),
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
|
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
knockAction = knockAction,
|
||||||
|
applicationName = "AppName",
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun anInviteSender(
|
||||||
|
userId: UserId = UserId("@bob:domain"),
|
||||||
|
displayName: String = "Bob",
|
||||||
|
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||||
|
) = InviteSender(
|
||||||
|
userId = userId,
|
||||||
|
displayName = displayName,
|
||||||
|
avatarData = avatarData,
|
||||||
|
)
|
||||||
|
|
||||||
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||||
|
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
||||||
|
|
|
||||||
|
|
@ -16,62 +16,78 @@
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.button.SuperButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun JoinRoomView(
|
fun JoinRoomView(
|
||||||
state: JoinRoomState,
|
state: JoinRoomState,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
|
onKnockSuccess: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
LightGradientBackground()
|
||||||
HeaderFooterPage(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
containerColor = Color.Transparent,
|
||||||
paddingValues = PaddingValues(16.dp),
|
paddingValues = PaddingValues(16.dp),
|
||||||
topBar = {
|
topBar = {
|
||||||
JoinRoomTopBar(onBackClicked = onBackPressed)
|
JoinRoomTopBar(onBackClicked = onBackPressed)
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
JoinRoomContent(contentState = state.contentState)
|
JoinRoomContent(
|
||||||
|
contentState = state.contentState,
|
||||||
|
applicationName = state.applicationName,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
JoinRoomFooter(
|
JoinRoomFooter(
|
||||||
joinAuthorisationStatus = state.joinAuthorisationStatus,
|
state = state,
|
||||||
onAcceptInvite = {
|
onAcceptInvite = {
|
||||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
state.eventSink(JoinRoomEvents.AcceptInvite)
|
||||||
},
|
},
|
||||||
|
|
@ -81,100 +97,173 @@ fun JoinRoomView(
|
||||||
onJoinRoom = {
|
onJoinRoom = {
|
||||||
state.eventSink(JoinRoomEvents.JoinRoom)
|
state.eventSink(JoinRoomEvents.JoinRoom)
|
||||||
},
|
},
|
||||||
|
onKnockRoom = {
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
},
|
||||||
|
onGoBack = onBackPressed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.knockAction,
|
||||||
|
onSuccess = { onKnockSuccess() },
|
||||||
|
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomFooter(
|
private fun JoinRoomFooter(
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus,
|
state: JoinRoomState,
|
||||||
onAcceptInvite: () -> Unit,
|
onAcceptInvite: () -> Unit,
|
||||||
onDeclineInvite: () -> Unit,
|
onDeclineInvite: () -> Unit,
|
||||||
onJoinRoom: () -> Unit,
|
onJoinRoom: () -> Unit,
|
||||||
|
onKnockRoom: () -> Unit,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onGoBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
if (state.contentState is ContentState.Failure) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_retry),
|
||||||
|
onClick = onRetry,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Large,
|
||||||
|
)
|
||||||
|
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_go_back),
|
||||||
|
onClick = onGoBack,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Large,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||||
when (joinAuthorisationStatus) {
|
when (joinAuthorisationStatus) {
|
||||||
JoinAuthorisationStatus.IsInvited -> {
|
is JoinAuthorisationStatus.IsInvited -> {
|
||||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
text = stringResource(CommonStrings.action_decline),
|
text = stringResource(CommonStrings.action_decline),
|
||||||
onClick = onDeclineInvite,
|
onClick = onDeclineInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_accept),
|
text = stringResource(CommonStrings.action_accept),
|
||||||
onClick = onAcceptInvite,
|
onClick = onAcceptInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.CanJoin -> {
|
JoinAuthorisationStatus.CanJoin -> {
|
||||||
Button(
|
SuperButton(
|
||||||
text = stringResource(R.string.screen_join_room_join_action),
|
|
||||||
onClick = onJoinRoom,
|
onClick = onJoinRoom,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
buttonSize = ButtonSize.Large,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_join_action),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
JoinAuthorisationStatus.CanKnock -> {
|
JoinAuthorisationStatus.CanKnock -> {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(R.string.screen_join_room_knock_action),
|
text = stringResource(R.string.screen_join_room_knock_action),
|
||||||
onClick = onJoinRoom,
|
onClick = onKnockRoom,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.Unknown -> Unit
|
JoinAuthorisationStatus.Unknown -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomContent(
|
private fun JoinRoomContent(
|
||||||
contentState: ContentState,
|
contentState: ContentState,
|
||||||
|
applicationName: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (contentState) {
|
when (contentState) {
|
||||||
is ContentState.Loaded -> {
|
is ContentState.Loaded -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Title(contentState.computedTitle)
|
if (contentState.name != null) {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = contentState.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = stringResource(id = CommonStrings.common_no_room_name),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
Subtitle(contentState.computedSubtitle)
|
if (contentState.alias != null) {
|
||||||
|
RoomPreviewSubtitleAtom(contentState.alias.value)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description = {
|
description = {
|
||||||
Description(contentState.topic ?: "")
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||||
|
if (inviteSender != null) {
|
||||||
|
InviteSenderView(inviteSender = inviteSender)
|
||||||
|
}
|
||||||
|
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||||
|
if (contentState.roomType == RoomType.Space) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
memberCount = {
|
memberCount = {
|
||||||
if (contentState.showMemberCount) {
|
if (contentState.showMemberCount) {
|
||||||
MembersCount(memberCount = contentState.numberOfMembers ?: 0)
|
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ContentState.UnknownRoom -> {
|
is ContentState.UnknownRoom -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Title(stringResource(R.string.screen_join_room_title_no_preview))
|
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ContentState.Loading -> {
|
is ContentState.Loading -> {
|
||||||
ContentScaffold(
|
RoomPreviewOrganism(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatar = {
|
avatar = {
|
||||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
|
|
@ -187,94 +276,31 @@ private fun JoinRoomContent(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
is ContentState.Failure -> {
|
||||||
}
|
RoomPreviewOrganism(
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentScaffold(
|
|
||||||
avatar: @Composable () -> Unit,
|
|
||||||
title: @Composable () -> Unit,
|
|
||||||
subtitle: @Composable () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
description: @Composable (() -> Unit)? = null,
|
|
||||||
memberCount: @Composable (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
avatar()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
title()
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
subtitle()
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
if (memberCount != null) {
|
|
||||||
memberCount()
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
if (description != null) {
|
|
||||||
description()
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Title(title: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = title,
|
avatar = {
|
||||||
style = ElementTheme.typography.fontHeadingMdBold,
|
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
when (contentState.roomIdOrAlias) {
|
||||||
|
is RoomIdOrAlias.Alias -> {
|
||||||
|
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
|
||||||
|
}
|
||||||
|
is RoomIdOrAlias.Id -> {
|
||||||
|
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.error_unknown),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Subtitle(subtitle: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
|
||||||
text = subtitle,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Description(description: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier,
|
|
||||||
text = description,
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MembersCount(memberCount: Long) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
|
|
||||||
.widthIn(min = 48.dp)
|
|
||||||
.padding(all = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.UserProfile(),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "$memberCount",
|
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,11 +317,12 @@ private fun JoinRoomTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = { }
|
onBackPressed = { },
|
||||||
|
onKnockSuccess = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -34,15 +36,24 @@ object JoinRoomModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun providesJoinRoomPresenterFactory(
|
fun providesJoinRoomPresenterFactory(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
|
knockRoom: KnockRoom,
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
buildMeta: BuildMeta,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
return object : JoinRoomPresenter.Factory {
|
return object : JoinRoomPresenter.Factory {
|
||||||
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
|
override fun create(
|
||||||
|
roomId: RoomId,
|
||||||
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
roomDescription: Optional<RoomDescription>,
|
||||||
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
|
knockRoom = knockRoom,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
buildMeta = buildMeta,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 io.element.android.features.joinroom.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface KnockRoom {
|
||||||
|
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Далучыцца да пакоя"</string>
|
<string name="screen_join_room_join_action">"Далучыцца да пакоя"</string>
|
||||||
<string name="screen_join_room_knock_action">"Націсніце, каб далучыцца"</string>
|
<string name="screen_join_room_knock_action">"Націсніце, каб далучыцца"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s пакуль не падтрымлівае прасторы. Вы можаце атрымаць доступ да прастор праз вэб-старонку."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Прасторы пакуль не падтрымліваюцца"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Націсніце кнопку ніжэй, і адміністратар пакоя атрымае апавяшчэнне. Вы зможаце далучыцца да размовы пасля зацвярджэння."</string>
|
<string name="screen_join_room_subtitle_knock">"Націсніце кнопку ніжэй, і адміністратар пакоя атрымае апавяшчэнне. Вы зможаце далучыцца да размовы пасля зацвярджэння."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Вы павінны быць удзельнікам гэтага пакоя каб прагледзець гісторыю паведамленняў."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Вы павінны быць удзельнікам гэтага пакоя каб прагледзець гісторыю паведамленняў."</string>
|
||||||
<string name="screen_join_room_title_knock">"Вы хочаце далучыцца да гэтага пакоя?"</string>
|
<string name="screen_join_room_title_knock">"Вы хочаце далучыцца да гэтага пакоя?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Připojit se do místnosti"</string>
|
<string name="screen_join_room_join_action">"Připojit se do místnosti"</string>
|
||||||
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>
|
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s zatím nepodporuje prostory. Prostory můžete používat na webu."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Prostory zatím nejsou podporovány"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string>
|
<string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Pro zobrazení historie zpráv musíte být členem této místnosti."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Pro zobrazení historie zpráv musíte být členem této místnosti."</string>
|
||||||
<string name="screen_join_room_title_knock">"Chcete se připojit k této místnosti?"</string>
|
<string name="screen_join_room_title_knock">"Chcete se připojit k této místnosti?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
||||||
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Raumadministrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
|
||||||
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</string>
|
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</string>
|
||||||
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
|
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Rejoindre"</string>
|
<string name="screen_join_room_join_action">"Rejoindre"</string>
|
||||||
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"Les Spaces ne sont pas encore pris en charge par %1$s . Vous pouvez voir les Spaces sur le Web."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Les Spaces ne sont pas encore pris en charge"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>
|
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Vous devez être un membre du salon pour pouvoir lire l’historique des messages."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Vous devez être un membre du salon pour pouvoir lire l’historique des messages."</string>
|
||||||
<string name="screen_join_room_title_knock">"Vous souhaitez rejoindre ce salon?"</string>
|
<string name="screen_join_room_title_knock">"Vous souhaitez rejoindre ce salon?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Csatlakozás a szobához"</string>
|
<string name="screen_join_room_join_action">"Csatlakozás a szobához"</string>
|
||||||
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>
|
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"Az %1$s még nem támogatja a tereket. A tereket a weben érheti el."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"A terek még nem támogatottak"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string>
|
<string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Az üzenetelőzmények megtekintéséhez a szoba tagjának kell lennie."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Az üzenetelőzmények megtekintéséhez a szoba tagjának kell lennie."</string>
|
||||||
<string name="screen_join_room_title_knock">"Csatlakozna ehhez a szobához?"</string>
|
<string name="screen_join_room_title_knock">"Csatlakozna ehhez a szobához?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Pripojiť sa do miestnosti"</string>
|
<string name="screen_join_room_join_action">"Pripojiť sa do miestnosti"</string>
|
||||||
<string name="screen_join_room_knock_action">"Zaklopaním sa pripojíte"</string>
|
<string name="screen_join_room_knock_action">"Zaklopaním sa pripojíte"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s zatiaľ nepodporuje priestory. K priestorom môžete pristupovať na webe."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Priestory zatiaľ nie sú podporované"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string>
|
<string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Ak chcete zobraziť históriu správ, musíte byť členom tejto miestnosti."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Ak chcete zobraziť históriu správ, musíte byť členom tejto miestnosti."</string>
|
||||||
<string name="screen_join_room_title_knock">"Chcete sa pripojiť do tejto miestnosti?"</string>
|
<string name="screen_join_room_title_knock">"Chcete sa pripojiť do tejto miestnosti?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Join room"</string>
|
<string name="screen_join_room_join_action">"Join room"</string>
|
||||||
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
|
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
|
||||||
<string name="screen_join_room_title_knock">"Want to join this room?"</string>
|
<string name="screen_join_room_title_knock">"Want to join this room?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,12 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface SeenInvitesStore {
|
class FakeKnockRoom(
|
||||||
fun seenRoomIds(): Flow<Set<RoomId>>
|
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
suspend fun markAsSeen(roomIds: Set<RoomId>)
|
) : KnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
|
||||||
}
|
}
|
||||||
|
|
@ -20,15 +20,27 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -49,9 +61,11 @@ class JoinRoomPresenterTest {
|
||||||
val presenter = createJoinRoomPresenter()
|
val presenter = createJoinRoomPresenter()
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID))
|
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
|
assertThat(state.applicationName).isEqualTo("AppName")
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +110,31 @@ class JoinRoomPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
|
||||||
|
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
|
||||||
|
val expectedInviteSender = inviter.toInviteSender()
|
||||||
|
val roomInfo = aRoomInfo(
|
||||||
|
currentUserMembership = CurrentUserMembership.INVITED,
|
||||||
|
inviter = inviter,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
getRoomInfoFlowLambda = { _ ->
|
||||||
|
flowOf(Optional.of(roomInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = matrixClient
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -237,16 +275,158 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - emit knock room event`() = runTest {
|
||||||
|
val knockRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val knockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||||
|
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
||||||
|
}
|
||||||
|
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
|
||||||
|
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
fakeKnockRoom.lambda = knockRoomFailure
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(knockRoomSuccess)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
assert(knockRoomFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.success(
|
||||||
|
RoomPreview(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
avatarUrl = "avatarUrl",
|
||||||
|
numberOfJoinedMembers = 2,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
isHistoryWorldReadable = false,
|
||||||
|
isJoined = false,
|
||||||
|
isInvited = false,
|
||||||
|
isPublic = true,
|
||||||
|
canKnock = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loaded(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
|
numberOfMembers = 2,
|
||||||
|
isDirect = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
roomAvatarUrl = "avatarUrl",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Failure(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
error = AN_EXCEPTION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Failure(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
error = AN_EXCEPTION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getRoomPreviewResult = {
|
||||||
|
Result.failure(Exception("403"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.UnknownRoom(
|
||||||
|
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createJoinRoomPresenter(
|
private fun createJoinRoomPresenter(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||||
|
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
||||||
): JoinRoomPresenter {
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
|
knockRoom = knockRoom,
|
||||||
|
buildMeta = buildMeta,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +435,7 @@ class JoinRoomPresenterTest {
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String? = A_ROOM_NAME,
|
name: String? = A_ROOM_NAME,
|
||||||
topic: String? = "A room about something",
|
topic: String? = "A room about something",
|
||||||
alias: String? = "#alias:matrix.org",
|
alias: RoomAlias? = RoomAlias("#alias:matrix.org"),
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
|
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||||
numberOfMembers: Long = 2L
|
numberOfMembers: Long = 2L
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class JoinRoomViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Join room on CanJoin room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_join_action)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Knock room on CanKnock room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_knock_action)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on closing Knock error emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||||
|
knockAction = AsyncAction.Failure(Exception("Error")),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Retry when an error occurs emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState(),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_retry)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Go back when a space is displayed invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(roomType = RoomType.Space),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_go_back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
|
||||||
|
state: JoinRoomState,
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
JoinRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
onKnockSuccess = onKnockSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="leave_conversation_alert_subtitle">"Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."</string>
|
<string name="leave_conversation_alert_subtitle">"Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du austritst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
|
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
|
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string>
|
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_conversation_alert_subtitle">"Är du säker på att du vill lämna den här konversationen? Den här konversationen är inte offentlig och du kommer inte att kunna gå med igen utan en inbjudan."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Är du säker på att du vill lämna det här rummet? Du är den enda personen här. Om du lämnar kommer ingen att kunna gå med i framtiden, inklusive du."</string>
|
<string name="leave_room_alert_empty_subtitle">"Är du säker på att du vill lämna det här rummet? Du är den enda personen här. Om du lämnar kommer ingen att kunna gå med i framtiden, inklusive du."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Är du säker på att du vill lämna det här rummet? Detta rum är inte offentligt och du kommer inte att kunna gå med igen utan en inbjudan."</string>
|
<string name="leave_room_alert_private_subtitle">"Är du säker på att du vill lämna det här rummet? Detta rum är inte offentligt och du kommer inte att kunna gå med igen utan en inbjudan."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Är du säker på att du vill lämna rummet?"</string>
|
<string name="leave_room_alert_subtitle">"Är du säker på att du vill lämna rummet?"</string>
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,7 @@ private fun PinUnlockFooter(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
PinUnlockView(
|
PinUnlockView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -369,7 +369,7 @@ internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider:
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
internal fun PinUnlockDefaultViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
PinUnlockView(
|
PinUnlockView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Bitte eine andere PIN verwenden."</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Bitte eine andere PIN verwenden."</string>
|
||||||
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf Deine Chats zu beschränken.
|
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf deine Chats zu beschränken.
|
||||||
|
|
||||||
Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string>
|
Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_app_lock_biometric_authentication">"Authentification biométrique"</string>
|
<string name="screen_app_lock_biometric_authentication">"authentification biométrique"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"Déverrouillage biométrique"</string>
|
<string name="screen_app_lock_biometric_unlock">"déverrouillage biométrique"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Déverrouiller avec la biométrie"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Déverrouiller avec la biométrie"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Code PIN oublié?"</string>
|
<string name="screen_app_lock_forgot_pin">"Code PIN oublié?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Modifier le code PIN"</string>
|
<string name="screen_app_lock_settings_change_pin">"Modifier le code PIN"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,27 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_app_lock_biometric_authentication">"biometrisk autentisering"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock">"biometrisk upplåsning"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock_title_android">"Lås upp med biometri"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Glömt PIN-kod?"</string>
|
<string name="screen_app_lock_forgot_pin">"Glömt PIN-kod?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
|
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Tillåt %1$s"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_skip">"Jag vill hellre använda PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Bespara dig själv lite tid och använd %1$s för att låsa upp appen varje gång"</string>
|
||||||
|
<string name="screen_app_lock_setup_choose_pin">"Välj PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_setup_confirm_pin">"Bekräfta PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Du kan inte välja detta som din PIN-kod av säkerhetsskäl"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Välj en annan PIN-kod"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_context">"Lås %1$s för att lägga till extra säkerhet i dina chattar.
|
||||||
|
|
||||||
|
Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från appen."</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Ange samma PIN-kod två gånger"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koder matchar inte"</string>
|
||||||
|
<string name="screen_app_lock_signout_alert_message">"Du måste logga in igen och skapa en ny PIN-kod för att fortsätta"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Du blir utloggad"</string>
|
<string name="screen_app_lock_signout_alert_title">"Du blir utloggad"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"Du har %1$d försök att låsa upp"</item>
|
<item quantity="one">"Du har %1$d försök att låsa upp"</item>
|
||||||
|
|
@ -15,5 +31,7 @@
|
||||||
<item quantity="one">"Fel PIN-kod. Du har %1$d försök kvar"</item>
|
<item quantity="one">"Fel PIN-kod. Du har %1$d försök kvar"</item>
|
||||||
<item quantity="other">"Fel PIN-kod. Du har %1$d försök kvar"</item>
|
<item quantity="other">"Fel PIN-kod. Du har %1$d försök kvar"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="screen_app_lock_use_biometric_android">"Använd biometri"</string>
|
||||||
|
<string name="screen_app_lock_use_pin_android">"Använd PIN-kod"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"確認 PIN 碼"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"確認 PIN 碼"</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"基於安全性的考量,您選的 PIN 碼無法使用"</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"基於安全性的考量,您選的 PIN 碼無法使用"</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"選擇不一樣的 PIN 碼"</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"選擇不一樣的 PIN 碼"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_context">"將 %1$s 上鎖,為你的聊天室添加一層防護。
|
||||||
|
|
||||||
|
請選擇好記憶的數字。如果忘記 PIN 碼,您會被登出。"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"請輸入相同的 PIN 碼兩次"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"請輸入相同的 PIN 碼兩次"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN 碼不一樣"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN 碼不一樣"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"您需要重新登入並建立新的 PIN 碼才能繼續"</string>
|
<string name="screen_app_lock_signout_alert_message">"您需要重新登入並建立新的 PIN 碼才能繼續"</string>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue