diff --git a/.editorconfig b/.editorconfig
index ac30459c21f..d2f28922d9b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,7 +14,7 @@ ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
-# Ktlint rule, for more information see https://pinterest.github.io/ktlint/1.1.1/faq/#how-do-i-enable-or-disable-a-rule
+# Ktlint rule, for more information see https://pinterest.github.io/ktlint/latest/faq/#how-do-i-enable-or-disable-a-rule
ktlint_standard_wrapping = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
@@ -26,7 +26,15 @@ ktlint_standard_annotation = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_indent = disabled
ktlint_standard_blank-line-before-declaration = disabled
-ktlint_function_naming_ignore_when_annotated_with=Composable
+ktlint_function_naming_ignore_when_annotated_with = Composable
+# Added when upgrading to 1.7.1
+ktlint_standard_function-expression-body = disabled
+ktlint_standard_chain-method-continuation = disabled
+ktlint_standard_class-signature = disabled
+# Added when upgrading to 1.8.0
+ktlint_standard_when-entry-bracing = disabled
+ktlint_standard_blank-line-between-when-conditions = disabled
+ktlint_standard_mixed-condition-operators = disabled
[*.java]
ij_java_align_consecutive_assignments = false
@@ -913,3 +921,8 @@ ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true
+
+[**/generated/**]
+generated_code = true
+ij_formatter_enabled = false
+ktlint = disabled
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index 5f13ff4efb2..0a1fc8653dc 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,5 @@
screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
+libraries/compound/screenshots/** filter=lfs diff=lfs merge=lfs -text
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ec67e56676b..7bda7136235 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,8 +10,7 @@ updates:
directory: "/"
schedule:
interval: "daily"
- ignore:
- - dependency-name: "*"
+ open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
# Updates for Gradle dependencies used in the app
@@ -19,8 +18,6 @@ updates:
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 200
- ignore:
- - dependency-name: "*"
+ open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1668cd2b1fb..6c77cb37708 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,18 +25,18 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
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: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs
@@ -53,7 +53,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-debug
path: |
@@ -61,7 +61,7 @@ jobs:
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
- name: Upload x86_64 APK for Maestro
if: ${{ matrix.variant == 'debug' }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-apk-maestro
path: |
@@ -69,7 +69,7 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- - uses: rnkdsh/action-upload-diawi@605adbad0db6c000eee26adfd8fc128d7df8f7ab # v1.5.9
+ - uses: rnkdsh/action-upload-diawi@4e1421305be7cfc510d05f47850262eeaf345108 # v1.5.12
id: diawi
# Do not fail the whole build if Diawi upload fails
continue-on-error: true
diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml
index a1ec5063c0d..bce9d923f33 100644
--- a/.github/workflows/build_enterprise.yml
+++ b/.github/workflows/build_enterprise.yml
@@ -27,7 +27,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-enterprise-{0}-{1}', matrix.variant, github.sha) || format('build-enterprise-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -39,12 +39,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK
@@ -61,7 +61,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-enterprise-debug
path: |
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 9de62e104c1..e4300b3321e 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -9,7 +9,7 @@ jobs:
# Skip in forks, it doesn't work even with the fallback token
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
@@ -20,7 +20,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
- uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
+ uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
diff --git a/.github/workflows/fork-pr-notice.yml b/.github/workflows/fork-pr-notice.yml
index 8c06c708d5e..79d33d3b6dd 100644
--- a/.github/workflows/fork-pr-notice.yml
+++ b/.github/workflows/fork-pr-notice.yml
@@ -15,7 +15,7 @@ jobs:
if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name
steps:
- name: Add auto-generated commit warning
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml
index 1f5551ca8ca..008ac299c7d 100644
--- a/.github/workflows/generate_github_pages.yml
+++ b/.github/workflows/generate_github_pages.yml
@@ -14,18 +14,18 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.12
+ python-version: 3.14
- name: Run World screenshots generation script
run: |
./tools/test/generateWorldScreenshots.py
diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml
index dcc7bc5f0a3..f826bb82051 100644
--- a/.github/workflows/gradle-wrapper-update.yml
+++ b/.github/workflows/gradle-wrapper-update.yml
@@ -11,8 +11,8 @@ jobs:
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
+ - uses: actions/checkout@v5
+ - uses: actions/setup-java@v5
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml
index cfb743934f2..ff0ac49f5c1 100644
--- a/.github/workflows/maestro-local.yml
+++ b/.github/workflows/maestro-local.yml
@@ -23,18 +23,18 @@ jobs:
group: ${{ format('maestro-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
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.ref }}
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v5
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
@@ -44,7 +44,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-apk-maestro
path: |
@@ -62,14 +62,14 @@ jobs:
group: ${{ format('maestro-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
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.ref }}
- name: Download APK artifact from previous job
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v6
with:
name: elementx-apk-maestro
- name: Enable KVM group perms
@@ -102,7 +102,7 @@ jobs:
script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
- name: Upload test results
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: test-results
path: |
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index d82129e18d2..b494d9fe8c5 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -16,9 +16,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml
index 7cc4f766c21..080cf99b05d 100644
--- a/.github/workflows/nightlyReports.yml
+++ b/.github/workflows/nightlyReports.yml
@@ -21,13 +21,13 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: false
@@ -42,7 +42,7 @@ jobs:
- name: ✅ Upload kover report
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: kover-results
path: |
@@ -60,21 +60,21 @@ jobs:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: dependency-analysis
path: build/reports/dependency-check-report.html
diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml
index 56d40d0cf6c..a5c2504f8e2 100644
--- a/.github/workflows/post-release.yml
+++ b/.github/workflows/post-release.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Trigger pipeline
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }}
script: |
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 929bcb5dcd0..6063fe00992 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -15,7 +15,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -39,7 +39,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
github.rest.issues.addLabels({
@@ -58,7 +58,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index bb6bbe87d1b..7600d8965c7 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -17,7 +17,7 @@ jobs:
name: Search for forbidden patterns
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@@ -33,11 +33,11 @@ jobs:
name: Search for invalid screenshot files
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.12
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.12
+ python-version: 3.14
- name: Search for invalid screenshot files
run: ./tools/test/checkInvalidScreenshots.py
@@ -45,20 +45,20 @@ jobs:
name: Search for invalid dependencies
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.12
+ python-version: 3.14
- name: Search for invalid dependencies
run: ./tools/dependencies/checkDependencies.py
@@ -71,7 +71,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -85,24 +85,57 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: konsist-report
path: |
**/build/reports/**/*.*
+ compose:
+ name: Compose tests
+ runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v5
+ 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: Add SSH private keys for submodule repositories
+ uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
+ if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
+ with:
+ ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
+ - name: Clone submodules
+ if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
+ run: git submodule update --init --recursive
+ - name: Use JDK 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin' # See 'Supported distributions' for available options
+ java-version: '21'
+ - name: Configure gradle
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
+ - name: Run compose tests
+ run: ./tools/compose/check_stability.sh
+
lint:
name: Android lint check
runs-on: ubuntu-latest
@@ -111,7 +144,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -125,12 +158,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@@ -141,7 +174,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: linting-report
path: |
@@ -155,7 +188,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -169,19 +202,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: detekt-report
path: |
@@ -195,7 +228,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -209,19 +242,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: ktlint-report
path: |
@@ -235,7 +268,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -249,12 +282,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit
@@ -266,7 +299,7 @@ jobs:
name: Check shell scripts
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run shellcheck
uses: ludeeus/action-shellcheck@2.0.0
with:
@@ -278,13 +311,13 @@ jobs:
needs: [konsist, lint, ktlint, detekt]
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
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 reports from previous jobs
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v6
- name: Prepare Danger
if: always()
run: |
@@ -293,7 +326,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
- uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
+ uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml
index a5172188ee0..bbcbef04d85 100644
--- a/.github/workflows/recordScreenshots.yml
+++ b/.github/workflows/recordScreenshots.yml
@@ -34,13 +34,13 @@ jobs:
with:
persist-credentials: false
- name: ☕️ Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 704802258ee..29ff4a5373d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,14 +18,14 @@ jobs:
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -38,7 +38,7 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-app-gplay-bundle-unsigned
path: |
@@ -52,7 +52,7 @@ jobs:
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@@ -61,12 +61,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -74,7 +74,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-enterprise-app-gplay-bundle-unsigned
path: |
@@ -87,14 +87,14 @@ jobs:
group: ${{ format('build-release-main-fdroid-{0}', github.sha) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -102,7 +102,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: elementx-app-fdroid-apks-unsigned
path: |
diff --git a/.github/workflows/scripts/maestro/local-recording.sh b/.github/workflows/scripts/maestro/local-recording.sh
index 6534d2feca3..adc83f48760 100755
--- a/.github/workflows/scripts/maestro/local-recording.sh
+++ b/.github/workflows/scripts/maestro/local-recording.sh
@@ -1,9 +1,10 @@
#!/bin/sh
#
+# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2024 New Vector Ltd.
#
-# SPDX-License-Identifier: AGPL-3.0-only
+# SPDX-License-Identifier: AGPL-3.0-only.
# Please see LICENSE in the repository root for full details.
#
diff --git a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh
index b94958dba4a..a9f789a4f5e 100755
--- a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh
+++ b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh
@@ -1,9 +1,10 @@
#!/bin/sh
#
+# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2024 New Vector Ltd.
#
-# SPDX-License-Identifier: AGPL-3.0-only
+# SPDX-License-Identifier: AGPL-3.0-only.
# Please see LICENSE in the repository root for full details.
#
diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh
index a610c6cc693..d29353a2c2f 100755
--- a/.github/workflows/scripts/recordScreenshots.sh
+++ b/.github/workflows/scripts/recordScreenshots.sh
@@ -1,8 +1,9 @@
#!/bin/bash
+# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2023-2024 New Vector Ltd.
#
-# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
# Please see LICENSE files in the repository root for full details.
set -e
@@ -56,6 +57,12 @@ echo "Deleting previous screenshots"
echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
+echo "Deleting previous screenshots"
+./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS
+
+echo "Record screenshots (Compound)"
+./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS
+
echo "Committing changes"
git config http.sslVerify false
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index c3b8cdf497b..acfe6ba87f7 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -22,18 +22,18 @@ jobs:
group: ${{ format('sonar-{0}', github.ref) }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
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: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures
diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml
index 3e9e4692a43..1f22bf2868b 100644
--- a/.github/workflows/sync-localazy.yml
+++ b/.github/workflows/sync-localazy.yml
@@ -11,20 +11,20 @@ jobs:
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.12
+ python-version: 3.14
- name: Setup Localazy
run: |
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
diff --git a/.github/workflows/sync-sas-strings.yml b/.github/workflows/sync-sas-strings.yml
index f9224853fd5..a73592466ec 100644
--- a/.github/workflows/sync-sas-strings.yml
+++ b/.github/workflows/sync-sas-strings.yml
@@ -12,11 +12,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.12
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.12
+ python-version: 3.14
- name: Install Prerequisite dependencies
run: |
pip install requests
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a0ffe74f6f1..4965530b5ad 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -47,12 +47,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
@@ -61,7 +61,7 @@ jobs:
- name: 🚫 Upload kover failed coverage reports
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: kover-error-report
path: |
@@ -73,16 +73,17 @@ jobs:
- name: 🚫 Upload test results on error
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: tests-and-screenshot-tests-results
path: |
**/build/paparazzi/failures/
+ **/build/roborazzi/failures/
**/build/reports/tests/*UnitTest/
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
+ uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index d4bdf725fc4..77acaadb20d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
+# Copyright (c) 2025 Element Creations Ltd.
+#
+# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+# Please see LICENSE files in the repository root for full details.
+
# Built application files
*.apk
*.ap_
@@ -47,8 +52,10 @@ captures/
.idea/compiler.xml
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
+.idea/deviceManager.xml
.idea/gradle.xml
.idea/jarRepositories.xml
+.idea/markdown.xml
.idea/misc.xml
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
@@ -61,6 +68,7 @@ captures/
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/copilot
+.idea/copilot.*
.idea/inspectionProfiles
# Shelved changes in the IDE
.idea/shelf
diff --git a/.idea/copyright/Element_Enterprise.xml b/.idea/copyright/Element_Enterprise.xml
index e8c30197324..b556049368f 100644
--- a/.idea/copyright/Element_Enterprise.xml
+++ b/.idea/copyright/Element_Enterprise.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/copyright/Element_FOSS.xml b/.idea/copyright/Element_FOSS.xml
index 930b4d9f5bc..bfc80c44e1f 100644
--- a/.idea/copyright/Element_FOSS.xml
+++ b/.idea/copyright/Element_FOSS.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 03fcfb7bea5..3efb2d8dd4c 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml
index f1f45527095..a16322543f9 100644
--- a/.maestro/tests/account/verifySession.yaml
+++ b/.maestro/tests/account/verifySession.yaml
@@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard
- tapOn: "Continue"
- extendedWaitUntil:
- visible: "Verification complete"
+ visible: "Device verified"
timeout: 30000
- tapOn: "Continue"
diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml
index e957d04bdf9..a72fb800759 100644
--- a/.maestro/tests/roomList/createAndDeleteRoom.yaml
+++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml
@@ -3,18 +3,18 @@ appId: ${MAESTRO_APP_ID}
# Purpose: Test the creation and deletion of a room
- tapOn: "Create a new conversation or room"
- tapOn: "New room"
-- tapOn: "Search for someone"
-- inputText: ${MAESTRO_INVITEE1_MXID}
-- tapOn:
- text: ${MAESTRO_INVITEE1_MXID}
- index: 1
-- tapOn: "Next"
- tapOn: "e.g. your project name"
- inputText: "aRoomName"
- tapOn: "What is this room about?"
- inputText: "aRoomTopic"
- tapOn: "Create"
- takeScreenshot: build/maestro/320-createAndDeleteRoom
+- tapOn: "Search for someone"
+- inputText: ${MAESTRO_INVITEE1_MXID}
+- tapOn:
+ text: ${MAESTRO_INVITEE1_MXID}
+ index: 1
+- tapOn: "Finish"
- tapOn: "aRoomName"
- tapOn: "Invite"
# assert there's 1 member and 1 invitee
diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml
index 5d5e01de84d..09197f058b8 100644
--- a/.maestro/tests/roomList/searchRoomList.yaml
+++ b/.maestro/tests/roomList/searchRoomList.yaml
@@ -5,6 +5,14 @@ appId: ${MAESTRO_APP_ID}
- inputText: ${MAESTRO_ROOM_NAME.substring(0, 3)}
- takeScreenshot: build/maestro/400-SearchRoom
- tapOn: ${MAESTRO_ROOM_NAME}
-# Back from timeline
+# Back from timeline to search
+- back
+- extendedWaitUntil:
+ visible: ${MAESTRO_ROOM_NAME.substring(0, 3)}
+ timeout: 10000
+# Back to close the keyboard
+- back
+- waitForAnimationToEnd
+# Back to close the home screen
- back
- runFlow: ../assertions/assertHomeDisplayed.yaml
diff --git a/.maestro/tests/roomList/timeline/messages/text.yaml b/.maestro/tests/roomList/timeline/messages/text.yaml
index cae5e310c02..4e1f4bce28f 100644
--- a/.maestro/tests/roomList/timeline/messages/text.yaml
+++ b/.maestro/tests/roomList/timeline/messages/text.yaml
@@ -4,6 +4,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
id: "text_editor"
- inputText: "Hello world!"
-- tapOn: "Send"
+- tapOn: "Send message"
- hideKeyboard
- takeScreenshot: build/maestro/511-Timeline
diff --git a/.maestro/tests/roomList/timeline/timeline.yaml b/.maestro/tests/roomList/timeline/timeline.yaml
index d82c5ea1ff4..0ad9231673b 100644
--- a/.maestro/tests/roomList/timeline/timeline.yaml
+++ b/.maestro/tests/roomList/timeline/timeline.yaml
@@ -6,6 +6,9 @@ appId: ${MAESTRO_APP_ID}
- runFlow: messages/text.yaml
- runFlow: messages/location.yaml
- runFlow: messages/poll.yaml
-- runFlow: call/call.yaml
+
+# Restore once the call flow is fixed
+#- runFlow: call/call.yaml
+
- back
- runFlow: ../../assertions/assertHomeDisplayed.yaml
diff --git a/CHANGES.md b/CHANGES.md
index 4b0b22eb038..28e774a8fff 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,693 @@
+Changes in Element X v25.11.2
+=============================
+
+
+
+## What's Changed
+### ✨ Features
+* Enable access to security and privacy by @bmarty in https://github.com/element-hq/element-x-android/pull/5566
+* Add ability to forward a media from the media viewer and the gallery by @bmarty in https://github.com/element-hq/element-x-android/pull/5622
+* Split notifications for messages in threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5595
+### 🙌 Improvements
+* Enable `SyncNotificationsWithWorkManager` in nightly and debug builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5573
+* Confirm exit without saving change in room details edit screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5618
+* Space : add view members entry by @ganfra in https://github.com/element-hq/element-x-android/pull/5619
+* Update notification sound by @bmarty in https://github.com/element-hq/element-x-android/pull/5667
+* Use the new notification sound only on debug and nightly build by @bmarty in https://github.com/element-hq/element-x-android/pull/5673
+* Make sure we know the session verification state before showing the options to verify the session by @bmarty in https://github.com/element-hq/element-x-android/pull/5677
+### 🐛 Bugfixes
+* Improve how brand color is applied. by @bmarty in https://github.com/element-hq/element-x-android/pull/5584
+* Improve wellknown retrieval API by @bmarty in https://github.com/element-hq/element-x-android/pull/5587
+* Clearing the room list search clears the search term too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5603
+* Delete pin code only when the last session is deleted by @bmarty in https://github.com/element-hq/element-x-android/pull/5600
+* Fix issues with WorkManager on Android 12 and below by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5606
+* Fix marking a room as read re-instantiates its timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5628
+* Display only valid emojis in recent emoji list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5612
+* Fix navigation issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/5666
+* Fix forward events from media viewer from pinned media timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/5669
+* Try fixing 'Timeline Event object has already been destroyed' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5675
+* Use the SDK Client to check whether a homeserver is compatible by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5664
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5610
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5662
+### 🧱 Build
+* Remove `@Inject`, not necessary anymore when class is annotated with `@ContributesBinding` by @bmarty in https://github.com/element-hq/element-x-android/pull/5589
+* Upgrade ktlint to 1.7.1 and ensure Renovate will upgrade the version by @bmarty in https://github.com/element-hq/element-x-android/pull/5638
+* Improve architecture around Nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5641
+* Move dependencies block out of the android block. by @bmarty in https://github.com/element-hq/element-x-android/pull/5674
+* Always use the handleEvent(s) function the same way. by @bmarty in https://github.com/element-hq/element-x-android/pull/5672
+### Dependency upgrades
+* fix(deps): update metro to v0.7.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5576
+* fix(deps): update dependencyanalysis to v3.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5577
+* fix(deps): update dependency io.sentry:sentry-android to v8.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5586
+* fix(deps): update dependency androidx.work:work-runtime-ktx to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5590
+* fix(deps): update dependency com.posthog:posthog-android to v3.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5594
+* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5572
+* Update plugin sonarqube to v7.0.1.6134 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5605
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5620
+* fix(deps): update dependencyanalysis to v3.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5602
+* fix(deps): update dependency com.github.matrix-org:matrix-analytics-events to v0.29.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5621
+* fix(deps): update dependencyanalysis to v3.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5624
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.29 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5625
+* fix(deps): update dependency io.sentry:sentry-android to v8.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5629
+* fix(deps): update dependencyanalysis to v3.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5642
+* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5644
+* chore(deps): update danger/danger-js action to v13.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5652
+* fix(deps): update dependency com.google.firebase:firebase-bom to v34.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5643
+* fix(deps): update firebaseappdistribution to v5.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5640
+* fix(deps): update metro to v0.7.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5663
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.31 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5657
+* Update GitHub Artifact Actions (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5609
+* Update dependency io.element.android:element-call-embedded to v0.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5598
+* Update roborazzi to v1.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5676
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.11.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5681
+* fix(deps): update metro to v0.7.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5683
+### Others
+* Improve code around Element .well-known configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/5565
+* misc: display offline banner for all LoggedIn screens by @ganfra in https://github.com/element-hq/element-x-android/pull/5574
+* Remove icon preview duplicate by @bmarty in https://github.com/element-hq/element-x-android/pull/5588
+* Remove application navigation state usage in the push module by @bmarty in https://github.com/element-hq/element-x-android/pull/5596
+* Design : update Home TopBar and RoomList Filters by @ganfra in https://github.com/element-hq/element-x-android/pull/5599
+* Add missing tests on the analytic modules by @bmarty in https://github.com/element-hq/element-x-android/pull/5604
+* design(space): let SpaceRoomItemView divider be full width by @ganfra in https://github.com/element-hq/element-x-android/pull/5597
+* Update notification style by @bmarty in https://github.com/element-hq/element-x-android/pull/5607
+* Improve how data is handled for the WorkManager. by @bmarty in https://github.com/element-hq/element-x-android/pull/5592
+* Revert "Make sure declining a call stops observing the ringing call state" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5615
+* Misc : space flow inject room by @ganfra in https://github.com/element-hq/element-x-android/pull/5614
+* Enable `SyncNotificationsWithWorkManager` by default in release mode apps too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5646
+* Revert "Update notification sound" by @bmarty in https://github.com/element-hq/element-x-android/pull/5671
+* Introduce new query to count accounts by @bmarty in https://github.com/element-hq/element-x-android/pull/5678
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.11.0...v25.11.2
+
+Changes in Element X v25.11.0
+=============================
+
+Hotfix release.
+
+Includes https://github.com/element-hq/element-x-android/pull/5615, which fixes an issue that prevented Element Call notifications from being displayed sometimes.
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.10.1...v25.11.0
+
+Changes in Element X v25.10.1
+=============================
+
+
+
+## What's Changed
+### ✨ Features
+* Sync notifications using WorkManager by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5545
+### 🙌 Improvements
+* Sort feature flags by @bmarty in https://github.com/element-hq/element-x-android/pull/5557
+### 🐛 Bugfixes
+* Makes sure images are loaded when cancelling multiaccount flow by @ganfra in https://github.com/element-hq/element-x-android/pull/5502
+* Fix 'test push loop back' notification check by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5541
+* Display 'join anyway' button on room preview when the state can't be loaded by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/5514
+* Fix media viewer not being dismissed with reduced motion enabled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5555
+* Keep the cursor position in room list search when going back by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5570
+* Make sure declining a call stops observing the ringing call state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5563
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5515
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5562
+### 🧱 Build
+* Do some cleanup on our immutable annotation usage by @bmarty in https://github.com/element-hq/element-x-android/pull/5503
+* `interface TestParameterValuesProvider` is deprecated. by @bmarty in https://github.com/element-hq/element-x-android/pull/5568
+### Dependency upgrades
+* fix(deps): update metro to v0.6.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5480
+* fix(deps): update dependency org.unifiedpush.android:connector to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5443
+* fix(deps): update wysiwyg to v2.40.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5400
+* fix(deps): update dependency io.github.sergio-sastre.composablepreviewscanner:android to v0.7.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5510
+* fix(deps): update camera to v1.5.1 - autoclosed by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5509
+* chore(deps): update plugin dependencycheck to v12.1.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5518
+* chore(deps): update plugin licensee to v1.14.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5477
+* chore(deps): update dependency python to 3.14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5475
+* fix(deps): update metro to v0.6.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5520
+* fix(deps): update dependency org.unifiedpush.android:connector to v3.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5519
+* chore(deps): update plugin gms_google_services to v4.4.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5507
+* fix(deps): update dependency com.google.firebase:firebase-bom to v34.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5522
+* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5524
+* fix(deps): update dependency net.zetetic:sqlcipher-android to v4.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5525
+* fix(deps): update dependencyanalysis to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5523
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5527
+* chore(deps): update plugin dependencycheck to v12.1.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5531
+* chore(deps): update rnkdsh/action-upload-diawi action to v1.5.12 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5533
+* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5548
+* fix(deps): update metro to v0.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5554
+* fix(deps): update dependency com.posthog:posthog-android to v3.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5564
+* chore(deps): update plugin sonarqube to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5535
+### Others
+* Import Compound tokens - fixed icons by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5506
+* Replace Uri by String in States that are used in Composable function. by @bmarty in https://github.com/element-hq/element-x-android/pull/5508
+* Let room filters follow the design. by @bmarty in https://github.com/element-hq/element-x-android/pull/5526
+* Allow uploading notification push rules in bug reports by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5538
+* Add number of accounts info in the rageshake data. by @bmarty in https://github.com/element-hq/element-x-android/pull/5532
+* design(space): match figma for Space views by @ganfra in https://github.com/element-hq/element-x-android/pull/5540
+* Extract console message logger and mutualize instance of Json by @bmarty in https://github.com/element-hq/element-x-android/pull/5552
+* Improve colors customization by @bmarty in https://github.com/element-hq/element-x-android/pull/5542
+* Fix test warning by @bmarty in https://github.com/element-hq/element-x-android/pull/5558
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.10.0...v25.10.1
+
+Changes in Element X v25.10.0
+=============================
+
+
+
+## What's Changed
+### ✨ Features
+* Use shared recent emoji reactions from account data by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5402
+* Follow permalinks to and from threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5414
+* Add support for Spaces by @bmarty in https://github.com/element-hq/element-x-android/pull/5462
+* Add Labs screen for beta testing of public features by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5465
+### 🙌 Improvements
+* Update the strings for the device verification flow by @andybalaam in https://github.com/element-hq/element-x-android/pull/5419
+* Set a notification sound by @bmarty in https://github.com/element-hq/element-x-android/pull/5469
+* Improve current push provider test: give info about the distributor. by @bmarty in https://github.com/element-hq/element-x-android/pull/5471
+* Improve AnnouncementService. by @bmarty in https://github.com/element-hq/element-x-android/pull/5482
+### 🐛 Bugfixes
+* Improvement and bugfix on incoming verification request screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5426
+* Space : makes sure to use room heroes for avatar by @ganfra in https://github.com/element-hq/element-x-android/pull/5488
+* Filter out direct room in the leave space screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/5498
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5427
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5460
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5486
+### 🧱 Build
+* Remove unused dependency on `javax.inject:javax.inject` by @bmarty in https://github.com/element-hq/element-x-android/pull/5445
+* Internalize compound-android by @bmarty in https://github.com/element-hq/element-x-android/pull/5457
+### 🚧 In development 🚧
+* Sdk : use latest apis for space by @ganfra in https://github.com/element-hq/element-x-android/pull/5404
+* Multi accounts - experimental first implementation by @bmarty in https://github.com/element-hq/element-x-android/pull/5285
+* Leave space - UI by @bmarty in https://github.com/element-hq/element-x-android/pull/5354
+* Leave spave: iteration on string value. by @bmarty in https://github.com/element-hq/element-x-android/pull/5425
+* Feature : space list join action by @ganfra in https://github.com/element-hq/element-x-android/pull/5431
+* Room list space invite by @ganfra in https://github.com/element-hq/element-x-android/pull/5449
+* Leave space: use SDK API. by @bmarty in https://github.com/element-hq/element-x-android/pull/5432
+* Space annoucement by @bmarty in https://github.com/element-hq/element-x-android/pull/5451
+* feature(space) : keep space children in the presenter by @ganfra in https://github.com/element-hq/element-x-android/pull/5456
+* Spaces : some tweaks around ui by @ganfra in https://github.com/element-hq/element-x-android/pull/5468
+* Use "BETA" word from Localazy and ensure layout is correct by @bmarty in https://github.com/element-hq/element-x-android/pull/5470
+* Disable avatar cluster for now by @bmarty in https://github.com/element-hq/element-x-android/pull/5492
+### Dependency upgrades
+* Update dependency com.posthog:posthog-android to v3.21.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5360
+* Update dependency io.element.android:element-call-embedded to v0.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5408
+* Update dependency net.java.dev.jna:jna to v5.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5398
+* Update plugin dependencycheck to v12.1.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5405
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5412
+* Update dependency androidx.sqlite:sqlite-ktx to v2.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5409
+* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5317
+* Update metro to v0.6.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5416
+* Update dependency app.cash.molecule:molecule-runtime to v2.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5413
+* Update dependency com.posthog:posthog-android to v3.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5415
+* Update metro to v0.6.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5422
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5438
+* fix(deps): update dependency net.java.dev.jna:jna to v5.18.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5437
+* fix(deps): update dependency io.mockk:mockk to v1.14.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5441
+* Update gradle/actions action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5444
+* fix(deps): update dependency io.sentry:sentry-android to v8.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5442
+* fix(deps): update dependency org.maplibre.gl:android-sdk to v12 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5455
+* fix(deps): update dependency com.posthog:posthog-android to v3.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5463
+* fix(deps): update roborazzi to v1.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5464
+* fix(deps): update telephoto to v0.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5459
+### Others
+* Ensure Metro `@AssistedInject` is used. by @bmarty in https://github.com/element-hq/element-x-android/pull/5420
+* Misc : destroy SpaceRoomList by @ganfra in https://github.com/element-hq/element-x-android/pull/5436
+* Remove CurrentSessionIdHolder and inject SessionId instead. by @bmarty in https://github.com/element-hq/element-x-android/pull/5440
+* Only offer to verify if a cross-signed device is available by @uhoreg in https://github.com/element-hq/element-x-android/pull/5433
+* Replace fun by val in MatrixClient by @bmarty in https://github.com/element-hq/element-x-android/pull/5466
+* Space : makes sure to use SpaceRoom.displayName from sdk by @ganfra in https://github.com/element-hq/element-x-android/pull/5476
+* Add preview with all icons in the Showkase browser by @bmarty in https://github.com/element-hq/element-x-android/pull/5485
+* Ensure that we are using Immutable instead of Persistent by @bmarty in https://github.com/element-hq/element-x-android/pull/5490
+* Reduce number of Previews for Avatar. by @bmarty in https://github.com/element-hq/element-x-android/pull/5495
+* Fix error when attempting to verify with recovery key with missing backup key by @uhoreg in https://github.com/element-hq/element-x-android/pull/5314
+* Sync strings by @bmarty in https://github.com/element-hq/element-x-android/pull/5499
+* feature(space): make sure to handle topic properly by @ganfra in https://github.com/element-hq/element-x-android/pull/5493
+
+## New Contributors
+* @uhoreg made their first contribution in https://github.com/element-hq/element-x-android/pull/5433
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.2...v25.10.0
+
+Changes in Element X v25.09.2
+=============================
+
+## What's Changed
+### ✨ Features
+* Show progress dialog while we are sending invites in a room by @richvdh in https://github.com/element-hq/element-x-android/pull/5342
+* Call: RTC decline event support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5305
+* Add room info to the thread's top app bar by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5374
+### 🙌 Improvements
+* Use the new RtcNotification event instead of the now deprecated CallNotify by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5357
+### 🐛 Bugfixes
+* Increase Element Call audio init delay ensuring the right audio device is used by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5315
+* Do not center the dialog title text for dialogs with no icon by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5332
+* Media viewer: release the `ExoPlayers` when the hosting composables are disposed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5351
+* Make PushData.clientSecret mandatory. by @bmarty in https://github.com/element-hq/element-x-android/pull/5369
+* Cleanup ftue code and ensure verification confirmation is displayed by @bmarty in https://github.com/element-hq/element-x-android/pull/5379
+* Change in clear cache behavior by @bmarty in https://github.com/element-hq/element-x-android/pull/5388
+* fix (room navigation) : fix navigation when leaving room/space by @ganfra in https://github.com/element-hq/element-x-android/pull/5376
+* fix (timeline) : forward pagination regression by @ganfra in https://github.com/element-hq/element-x-android/pull/5389
+* When joining a call, wait for the `content_loaded` action by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5399
+* Ensure the thread summary sender's display name won't wrap to the next line by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5403
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5349
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5385
+### 🧱 Build
+* Improve release script and the file Versions.kt by @bmarty in https://github.com/element-hq/element-x-android/pull/5318
+* Dependency: extract the Matrix SDK and add instructions for upgrading the library by @bmarty in https://github.com/element-hq/element-x-android/pull/5363
+* Add test on DefaultSpaceEntryPoint by @bmarty in https://github.com/element-hq/element-x-android/pull/5343
+### 🚧 In development 🚧
+* Space list by @bmarty in https://github.com/element-hq/element-x-android/pull/5320
+* Feature : Join Space (WIP) by @ganfra in https://github.com/element-hq/element-x-android/pull/5378
+### Dependency upgrades
+* Update activity to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5324
+* Update dependency com.google.truth:truth to v1.4.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5322
+* Update dependency io.sentry:sentry-android to v8.21.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5310
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5323
+* Update dependency androidx.sqlite:sqlite-ktx to v2.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5337
+* Update camera to v1.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5336
+* Update dependency com.posthog:posthog-android to v3.21.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5333
+* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5341
+* Upgrade Rust SDK bindings to v25.09.15 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5353
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.16 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5359
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5365
+* Update telephoto to v0.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5350
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5377
+* Update dependency com.google.firebase:firebase-bom to v34.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5367
+* Upgrade Element Call embedded dependency to `v0.16.0-rc.4` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5391
+* Update dependencyAnalysis to v3 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5194
+* Update dependency org.maplibre.gl:android-sdk to v11.13.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5381
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5396
+* Update plugin dependencycheck to v12.1.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5382
+* Update dependency io.sentry:sentry-android to v8.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5397
+### Others
+* Cleanup nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5358
+* Complete test on MediaGalleryPresenter by @bmarty in https://github.com/element-hq/element-x-android/pull/5361
+* Remove dead code by @bmarty in https://github.com/element-hq/element-x-android/pull/5306
+* Introduce BugReportFlowNode, and remove NavTarget.ViewLogs from RootFlowNode by @bmarty in https://github.com/element-hq/element-x-android/pull/5370
+* When logging out from Pin code screen, logout from all the sessions. by @bmarty in https://github.com/element-hq/element-x-android/pull/5372
+* Clean MatrixAuthenticationService and SessionStore API by @bmarty in https://github.com/element-hq/element-x-android/pull/5371
+* Add logs to detect duplicates in the room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5364
+* Add troubleshoot notification test about blocked users by @bmarty in https://github.com/element-hq/element-x-android/pull/5394
+* Add thread decoration with latest event details by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5355
+* Rework on messages view top bars by @bmarty in https://github.com/element-hq/element-x-android/pull/5401
+* Put developer settings at the end of the view by @p1gp1g in https://github.com/element-hq/element-x-android/pull/5387
+
+## New Contributors
+* @p1gp1g made their first contribution in https://github.com/element-hq/element-x-android/pull/5387
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.1...v25.09.2
+
+Changes in Element X v25.09.1
+=============================
+
+## What's Changed
+
+We have migrated our DI libraries from Dagger and Anvil to Metro. If you need more details on the migration steps, please read the [documentation](https://github.com/element-hq/element-x-android/blob/develop/docs/migration_to_metro.md).
+
+### ✨ Features
+* Allow replying to a message with an attachment by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5261
+* Add emoji search to the reaction emoji picker by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5255
+### 🙌 Improvements
+* Spelling correction in Update FeatureFlags.kt by @escix in https://github.com/element-hq/element-x-android/pull/5232
+* [a11y] Add content descriptions to room list item indicators by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5236
+* [a11y] Add click action to the message bottom sheet handle by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5228
+### 🐛 Bugfixes
+* Reload member list after moderation actions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5268
+* Restore view log code by @bmarty in https://github.com/element-hq/element-x-android/pull/5294
+* Detect mime type when picking a file by @bmarty in https://github.com/element-hq/element-x-android/pull/5291
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5249
+* Sync Strings - new translations to Korean by @ElementBot in https://github.com/element-hq/element-x-android/pull/5286
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5290
+### 🧱 Build
+* Iterate on build chain by @bmarty in https://github.com/element-hq/element-x-android/pull/5272
+* Cleanup our DI solution and add documentation about the migration to Metro by @bmarty in https://github.com/element-hq/element-x-android/pull/5287
+* Revert agp to 8.11 by @bmarty in https://github.com/element-hq/element-x-android/pull/5311
+### 🚧 In development 🚧
+* Space: add content in home screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5273
+* Hide the home navigation bar if the user is not a member of any Space. by @bmarty in https://github.com/element-hq/element-x-android/pull/5292
+### Dependency upgrades
+* Update dependency org.maplibre.gl:android-sdk to v11.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5239
+* Update dependency com.google.firebase:firebase-bom to v34.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5245
+* Update dependency com.posthog:posthog-android to v3.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5238
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5251
+* Update plugin sonarqube to v6.3.1.5724 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5235
+* Update android.gradle.plugin to v8.12.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5244
+* Update dependency io.element.android:emojibase-bindings to v1.4.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5250
+* Update actions/setup-python action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5270
+* Update dependency com.posthog:posthog-android to v3.21.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5275
+* Migrate Anvil KSP to Metro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5253
+* Update actions/github-script action to v8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5284
+* Update codecov/codecov-action action to v5.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5274
+* Update dependency io.sentry:sentry-android to v8.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5293
+### Others
+* Remove LoginUserStory. by @bmarty in https://github.com/element-hq/element-x-android/pull/5237
+* Update state in runUpdatingState when CancellationException occurs by @jbrenorv in https://github.com/element-hq/element-x-android/pull/5243
+* Refactor: Move InMemorySessionStore to test module by @bmarty in https://github.com/element-hq/element-x-android/pull/5252
+* Enable `largeHeap` option to have a larger max heap size by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5258
+* Set a custom request config for the Client by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5266
+* Set shortcut ID on received notifications to make them appear as a Conversation by @frebib in https://github.com/element-hq/element-x-android/pull/5192
+* Improve management of shortcut ids. by @bmarty in https://github.com/element-hq/element-x-android/pull/5303
+
+## New Contributors
+* @escix made their first contribution in https://github.com/element-hq/element-x-android/pull/5232
+* @jbrenorv made their first contribution in https://github.com/element-hq/element-x-android/pull/5243
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.09.0...v25.09.1
+
+Changes in Element X v25.09.0
+=============================
+
+This release is the same as `25.08.4` but it includes performance fixes for the timeline load times, included in the Rust SDK version upgrade and internal changes for Element Call.
+
+## What's Changed
+### 🧱 Build
+* Revert "Try following KSP incremental best practices on `anvilcodegen`" by @bmarty in https://github.com/element-hq/element-x-android/pull/5233
+### Dependency upgrades
+* Update dependency io.element.android:element-call-embedded to v0.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5229
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5230
+* Downgrade sonar scanner gradle plugin to `v6.2.0.5505` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5234
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.4...v25.09.0
+
+Changes in Element X v25.08.4
+=============================
+
+## What's Changed
+### ✨ Features
+* Threads - first iteration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5165
+* Add shortcut suggestions for rooms, remove then when leaving by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5180
+* Allow replying to any remote message in a thread by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5201
+### 🙌 Improvements
+* Create room flow rework by @bmarty in https://github.com/element-hq/element-x-android/pull/5166
+### 🐛 Bugfixes
+* Fix bitrate value used for video transcoding by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5183
+* Fix sending videos in Android 11 and lower by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5186
+* Ensure that only one DataStore is active for the same file. by @bmarty in https://github.com/element-hq/element-x-android/pull/5198
+* Handle preference stores corruption by clearing them by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5086
+* Use variable bitrate mode when transcoding to ensure compatibility with old devices by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5223
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5178
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5211
+### 🧱 Build
+* Build release with the latest build tools 36.0.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/5173
+* Try following KSP incremental best practices on `anvilcodegen` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5205
+* Split deeplink module and remove setupAnvil from api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/5210
+* Introduce a11y screenshot test by @bmarty in https://github.com/element-hq/element-x-android/pull/5214
+* Custom logo on on boarding screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/5217
+### 🚧 In development 🚧
+* Space UI component by @bmarty in https://github.com/element-hq/element-x-android/pull/5197
+* Add UI components for spaces. by @bmarty in https://github.com/element-hq/element-x-android/pull/5207
+### Dependency upgrades
+* Update core to v1.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5168
+* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5169
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5182
+* Update android.gradle.plugin to v8.12.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5184
+* Update dagger to v2.57.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5193
+* Update actions/setup-java action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5196
+* Update codecov/codecov-action action to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5191
+* Update plugin ktlint to v13.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5204
+* Update dependency com.posthog:posthog-android to v3.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5206
+* Update dependency org.jsoup:jsoup to v1.21.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5212
+* Update dependency com.posthog:posthog-android to v3.20.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5213
+* Update plugin sonarqube to v6.3.0.5676 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5220
+* Update dependency io.sentry:sentry-android to v8.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5216
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.8.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5219
+### Others
+* Iterate on invite people UI by @bmarty in https://github.com/element-hq/element-x-android/pull/5185
+* AnalyticsOptInStateProvider does not need to have an injected constructor by @bmarty in https://github.com/element-hq/element-x-android/pull/5215
+* Add extra logs for sending media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5218
+* Rename custom_logo to onboarding_logo by @bmarty in https://github.com/element-hq/element-x-android/pull/5226
+* Add unit test on VideoCompressorHelper by @bmarty in https://github.com/element-hq/element-x-android/pull/5227
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.3...v25.08.4
+
+Changes in Element X v25.08.3
+=============================
+
+## What's Changed
+### ✨ Features
+* Add media file limit size warning and media quality selection by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5131
+### 🐛 Bugfixes
+* Fix cursor position in room list search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5138
+* Fix leaving the room not always dismissing the room screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5089
+* Do not automatically initialize `DefaultVideoMetadataExtractor`'s data source by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5157
+* Provide calculated server names when opening a room from another by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5155
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5146
+### 🧱 Build
+* Compile and target sdk36 by @bmarty in https://github.com/element-hq/element-x-android/pull/5150
+* Fix Maestro regression when coming back from room to the search screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5156
+### Dependency upgrades
+* Update android.gradle.plugin to v8.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5106
+* Update wysiwyg to v2.39.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5080
+* Update dependency python to 3.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5144
+* Update rnkdsh/action-upload-diawi action to v1.5.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5141
+* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5143
+* Update actions/checkout action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5148
+* Update dependency io.sentry:sentry-android to v8.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5149
+* Update dependency io.sentry:sentry-android to v8.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5158
+* Update dependency androidx.browser:browser to v1.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5096
+* Update Compose bom to 2025.07.00 by @bmarty in https://github.com/element-hq/element-x-android/pull/5164
+* Update showkase to v1.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5117
+* Update haze to v1.6.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5167
+### Others
+* Let enterprise build be able to override (or disable) the bug report URL. by @bmarty in https://github.com/element-hq/element-x-android/pull/5139
+* Hide the recovery key while we are entering it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5147
+* Remove old feature flags by @bmarty in https://github.com/element-hq/element-x-android/pull/5160
+* Move push history entry point from notification settings to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/5161
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.2...v25.08.3
+
+Changes in Element X v25.08.2
+=============================
+
+
+
+## What's Changed
+### 🐛 Bugfixes
+* When mapping an invalid notification event, only drop that one by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5137
+### Dependency upgrades
+* Update dependency io.nlopez.compose.rules:detekt to v0.4.27 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5123
+* Update actions/download-artifact action to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5122
+* Update dependency net.zetetic:sqlcipher-android to v4.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5121
+* Update dependency com.posthog:posthog-android to v3.20.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5133
+* Update dependency com.google.firebase:firebase-bom to v34.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5136
+### Others
+* [a11y] Open context menu with the keyboard by @bmarty in https://github.com/element-hq/element-x-android/pull/5120
+* Let enterprise build store the logs in a dedicated subfolder by @bmarty in https://github.com/element-hq/element-x-android/pull/5132
+* Redirect FOSS user to Element Pro according to element .well-known file by @bmarty in https://github.com/element-hq/element-x-android/pull/5126
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.1...v25.08.2
+
+Changes in Element X v25.08.1
+=============================
+
+
+
+## What's Changed
+### 🙌 Improvements
+* Force last owner of a room to pass ownership when leaving by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5094
+### 🐛 Bugfixes
+* Reload room member list when active members count changes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5129
+* Delegate call notifications to Element Call, upgrade SDK and EC embedded by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5119
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5112
+### Dependency upgrades
+* Update media3 to v1.8.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5101
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.08.0...v25.08.1
+
+Changes in Element X v25.08.0
+=============================
+
+
+
+## What's Changed
+### 🐛 Bugfixes
+* Fix `toPlainText` where `
` tags appear by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5044
+* Remove the scaling added in `Player.Listener.onVideoSizeChanged` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5055
+* Make sure we clean up the pre-processed and uploaded media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5039
+* Calculate video output size taking into account portrait mode by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5068
+* Prevent loop when exiting the attachments preview screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5078
+* Prevent crash caused by re-release of wakelock in calls by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5077
+* Make sure we display errors when we create a recovery key and it fails by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5079
+* Fix crash when trying to get active notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5085
+* Adapt 'change roles' screens to the new creator/owner role by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5076
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5021
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5054
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5083
+### 🧱 Build
+* Disable Element Call Maestro tests for the time being by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5064
+### 📄 Documentation
+* Grammar fixes for docs and comments by @andybalaam in https://github.com/element-hq/element-x-android/pull/5043
+* Note how to switch back to the published SDK after building locally by @andybalaam in https://github.com/element-hq/element-x-android/pull/5042
+### Dependency upgrades
+* Update dependency io.mockk:mockk to v1.14.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5037
+* Update dependency androidx.lifecycle:lifecycle-process to v2.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5036
+* Update dagger to v2.57 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5038
+* Update haze to v1.6.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5045
+* Update dependency io.nlopez.compose.rules:detekt to v0.4.24 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5053
+* Update dependency io.nlopez.compose.rules:detekt to v0.4.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5058
+* Update coil to v3.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5063
+* Update dependency io.nlopez.compose.rules:detekt to v0.4.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5065
+* Update dependency com.posthog:posthog-android to v3.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5067
+* Update dependency com.google.firebase:firebase-bom to v34 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5061
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5073
+* Update dependency com.posthog:posthog-android to v3.20.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5087
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5088
+* Update dependency org.maplibre.gl:android-sdk to v11.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5093
+* Update dependency androidx.test:runner to v1.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5102
+* Update test.core to v1.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5104
+* Update dependency androidx.test.ext:junit to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5103
+* Update dependency io.sentry:sentry-android to v8.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5097
+### Others
+* Iterate on FloatingActionButton shape and colors. by @bmarty in https://github.com/element-hq/element-x-android/pull/5033
+* [a11y] Improve session verification screens by @bmarty in https://github.com/element-hq/element-x-android/pull/5017
+* misc (room id) : add room id regex pattern to match new versions by @ganfra in https://github.com/element-hq/element-x-android/pull/5040
+* Use lower level APIs to draw the message bubbles by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5056
+* misc (store description) : update store description for fastlane by @ganfra in https://github.com/element-hq/element-x-android/pull/5060
+* [a11y] Improve accessibility on avatar when creating a room. by @bmarty in https://github.com/element-hq/element-x-android/pull/5046
+* Add fallback notifications from UTDs to the push history by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5047
+* feature (media send queue) : enable send queue by default by @ganfra in https://github.com/element-hq/element-x-android/pull/5098
+* misc : re-enable share pos by default by @ganfra in https://github.com/element-hq/element-x-android/pull/5108
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.07.1...v25.08.0
+
+Changes in Element X v25.07.1
+=============================
+
+
+
+## What's Changed
+### 🐛 Bugfixes
+* fix ( room list) : rebuild with filteredSummaries to avoid bad state by @ganfra in https://github.com/element-hq/element-x-android/pull/4993
+* Keep video rotation metadata when transcoding by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5008
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4988
+### 🧱 Build
+* Update Gradle Wrapper from 8.14.2 to 8.14.3 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4985
+* Stop ignoring dependencies, but instead set `open-pull-requests-limit to 0 by @bmarty in https://github.com/element-hq/element-x-android/pull/5013
+### 📄 Documentation
+* Update to the status and clarifications with respect to the legacy app. by @mxandreas in https://github.com/element-hq/element-x-android/pull/5016
+### 🚧 In development 🚧
+* Home navigation bar fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/4990
+* Home screen iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/5003
+### Dependency upgrades
+* Update dependency io.element.android:compound-android to v25.7.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4984
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4989
+* Update plugin ktlint to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4992
+* Update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.7.1-0.6.x-compat by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4991
+* Update haze to v1.6.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4987
+* Update dependency com.squareup.okhttp3:okhttp-bom to v5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4979
+* Update dependency io.sentry:sentry-android to v8.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4998
+* Update dependency com.squareup.okhttp3:okhttp-bom to v5.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/4997
+* Update dependency org.maplibre.gl:android-sdk to v11.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5001
+* Update dependency com.posthog:posthog-android to v3.19.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5009
+* Update dependency org.maplibre.gl:android-sdk to v11.12.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5006
+* Update android.gradle.plugin to v8.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5014
+* Update rnkdsh/action-upload-diawi action to v1.5.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5019
+* Update wysiwyg to v2.38.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5025
+* Update haze to v1.6.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5026
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5011
+### Others
+* Remove bloom effect and replace by linear gradient by @bmarty in https://github.com/element-hq/element-x-android/pull/4926
+* misc (a11y) : mark MainActionButton icon as decorative by @ganfra in https://github.com/element-hq/element-x-android/pull/4996
+* Make `ContentAvoidingLayoutData` an immutable data class by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4999
+* Remove unused composable and cleanup colors by @bmarty in https://github.com/element-hq/element-x-android/pull/5000
+* Add a feature flag to reuse the last `pos` value for initial syncs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5010
+* [a11y] Fix several issues around accessibility by @bmarty in https://github.com/element-hq/element-x-android/pull/5007
+* Replace video transcoder with Media3 Transformer by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5018
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.07.0...v25.07.1
+
+Changes in Element X v25.07.0
+=============================
+
+
+
+## What's Changed
+### 🙌 Improvements
+* Change : handle invalid invite error by @ganfra in https://github.com/element-hq/element-x-android/pull/4909
+* Add ability to zoom on video. by @bmarty in https://github.com/element-hq/element-x-android/pull/4916
+* Change : sync moderation and safety preferences with server by @ganfra in https://github.com/element-hq/element-x-android/pull/4962
+### 🐛 Bugfixes
+* Restore `MarkdownEditText.focusSearch` override by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4908
+* Fix duplicate usage of a `modifier` variable in `TextInputBox` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4928
+### 🗣 Translations
+* Sync Strings - new translations to Danish by @ElementBot in https://github.com/element-hq/element-x-android/pull/4913
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4983
+### 🧱 Build
+* a11y: Add scripts to enable and disable the talkback service by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4906
+* Update min api level to 33 for Element enterprise by @bmarty in https://github.com/element-hq/element-x-android/pull/4960
+### 🚧 In development 🚧
+* Rename module roomlist to home by @bmarty in https://github.com/element-hq/element-x-android/pull/4955
+* Home navigation bar by @bmarty in https://github.com/element-hq/element-x-android/pull/4964
+### Dependency upgrades
+* fix(deps): update dependency org.unifiedpush.android:connector to v3.0.10 by @renovate in https://github.com/element-hq/element-x-android/pull/4871
+* fix(deps): update dependency io.sentry:sentry-android to v8.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4892
+* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4897
+* fix(deps): update wysiwyg to v2.38.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4907
+* fix(deps): update dependency org.robolectric:robolectric to v4.15 by @renovate in https://github.com/element-hq/element-x-android/pull/4901
+* fix(deps): update dependency androidx.sqlite:sqlite-ktx to v2.5.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4898
+* fix(deps): update dependency io.mockk:mockk to v1.14.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4912
+* fix(deps): update dependency org.robolectric:robolectric to v4.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4911
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.23 by @renovate in https://github.com/element-hq/element-x-android/pull/4917
+* fix(deps): update dependencyanalysis to v2.19.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4932
+* fix(deps): update dependency org.jsoup:jsoup to v1.21.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4914
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.25 by @renovate in https://github.com/element-hq/element-x-android/pull/4936
+* fix(deps): update dependency io.sentry:sentry-android to v8.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4938
+* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4939
+* fix(deps): update dependency com.google.firebase:firebase-bom to v33.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4945
+* fix(deps): update dependency io.sentry:sentry-android to v8.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4941
+* Update sdk to version 25.7.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/4966
+* Update haze to v1.6.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4968
+* Update dependency com.google.gms:google-services to v4.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4946
+* Update android.gradle.plugin to v8.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4931
+* Update dependency io.element.android:element-call-embedded to v0.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4969
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4967
+* Upgrade compose bom to 2025.06.01 by @bmarty in https://github.com/element-hq/element-x-android/pull/4970
+* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4918
+* Update dependency io.element.android:element-call-embedded to v0.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4977
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.3 by @ganfra in https://github.com/element-hq/element-x-android/pull/4976
+### Others
+* a11y: Make isTalkbackActive() live. by @bmarty in https://github.com/element-hq/element-x-android/pull/4903
+* a11y: improve accessibility on grouped state events header. by @bmarty in https://github.com/element-hq/element-x-android/pull/4902
+* Room debug info by @bmarty in https://github.com/element-hq/element-x-android/pull/4904
+* [a11y] Improve accessibility of message composer by @bmarty in https://github.com/element-hq/element-x-android/pull/4900
+* refactor: Migrate SQLCipher Android to new API by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4874
+* Iterate on avatar to be able to render Space avatar. by @bmarty in https://github.com/element-hq/element-x-android/pull/4921
+* Simplify syncing the room list when receiving a push by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4915
+* Add unit test on ChooseAccountProviderState so that the coverage is above 90% by @bmarty in https://github.com/element-hq/element-x-android/pull/4924
+* Iterate on avatar to be able to render Space avatar Part2 by @bmarty in https://github.com/element-hq/element-x-android/pull/4923
+* Introduce SessionEnterpriseService. by @bmarty in https://github.com/element-hq/element-x-android/pull/4925
+* Simplify message composer layout by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4884
+* Display error dialog if Element Call can't be joined by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4919
+* misc : simplify timeline diff logic by @ganfra in https://github.com/element-hq/element-x-android/pull/4930
+* Navigation bar component by @bmarty in https://github.com/element-hq/element-x-android/pull/4940
+* a11y: improve content description of the close buttons by @bmarty in https://github.com/element-hq/element-x-android/pull/4943
+* Element Call: remove top app bar and add it inside the webview instead by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4927
+* Replace the Report a problem button with the app's version on the on boading screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/4944
+* Split RoomListPresenter and introduce HomePresenter by @bmarty in https://github.com/element-hq/element-x-android/pull/4958
+* Add "View avatar" content description to all clickable Avatar that will open the avatar preview. by @bmarty in https://github.com/element-hq/element-x-android/pull/4948
+* [a11y] Ensure that the focus is not lost when the send button state change by @bmarty in https://github.com/element-hq/element-x-android/pull/4975
+* [a11y] add missing heading() qualifier on screen titles and other headers by @bmarty in https://github.com/element-hq/element-x-android/pull/4980
+* misc (tracing) : add new TraceLogPack.Notification by @ganfra in https://github.com/element-hq/element-x-android/pull/4981
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.3...v25.07.0
+
Changes in Element X v25.06.3
=============================
diff --git a/README.md b/README.md
index 20ac1c0536e..5406b158c92 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,15 @@
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
[](https://sonarcloud.io/summary/new_code?id=element-x-android)
-[](https://codecov.io/github/vector-im/element-x-android)
+[](https://codecov.io/github/element-hq/element-x-android)
[](https://matrix.to/#/#element-x-android:matrix.org)
[](https://localazy.com/p/element)
# Element X Android
-Element X Android is a [Matrix](https://matrix.org/) Android Client provided by [element.io](https://element.io/).
+Element X Android is the next-generation [Matrix](https://matrix.org/) client provided by [Element](https://element.io/).
-The application is a total rewrite of [Element-Android](https://github.com/element-hq/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
-
-Learn more about why we are building Element X in our blog post: [https://element.io/blog/element-x-experience-the-future-of-element/](https://element.io/blog/element-x-experience-the-future-of-element/).
+Compared to the previous-generation [Element Classic](https://github.com/element-hq/element-android), the application is a total rewrite, using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
[](https://play.google.com/store/apps/details?id=io.element.android.x)[](https://f-droid.org/packages/io.element.android.x)
@@ -72,7 +70,7 @@ We're doing this as a way to share code between platforms and while we've seen p
## Status
-This project is in an early rollout and migration phase.
+This project is actively developed and supported. New users are recommended to use Element X instead of the previous-generation app.
## Minimum SDK version
@@ -104,9 +102,10 @@ If after your research you still have a question, ask at [#element-x-android:mat
## Copyright and License
-Copyright (c) 2022 - 2025 New Vector Ltd
+Copyright (c) 2025 Element Creations Ltd.
+Copyright (c) 2022 - 2025 New Vector Ltd.
-This software is dual licensed by New Vector Ltd (Element). It can be used either:
+This software is dual licensed by Element Creations Ltd (Element). It can be used either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
diff --git a/anvilannotations/.gitignore b/annotations/.gitignore
similarity index 100%
rename from anvilannotations/.gitignore
rename to annotations/.gitignore
diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts
new file mode 100644
index 00000000000..33e3cbeff22
--- /dev/null
+++ b/annotations/build.gradle.kts
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ id("com.android.lint")
+}
diff --git a/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
similarity index 78%
rename from anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt
rename to annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
index d91a4761fc6..632bdc3e811 100644
--- a/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt
+++ b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.anvilannotations
+package io.element.android.annotations
import kotlin.reflect.KClass
@@ -13,7 +14,7 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph.
* Equivalent to the following declaration:
*
- * @Module
+ * @BindingContainer
* @ContributesTo(Scope::class)
* abstract class YourNodeModule {
diff --git a/anvilannotations/build.gradle.kts b/anvilannotations/build.gradle.kts
deleted file mode 100644
index 66b88b4dfa8..00000000000
--- a/anvilannotations/build.gradle.kts
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2022-2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-plugins {
- alias(libs.plugins.kotlin.jvm)
- id("com.android.lint")
-}
-
-dependencies {
- api(libs.inject)
-}
diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts
deleted file mode 100644
index 6f73bd9f443..00000000000
--- a/anvilcodegen/build.gradle.kts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2022-2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-plugins {
- alias(libs.plugins.kotlin.jvm)
-}
-
-dependencies {
- implementation(projects.anvilannotations)
- api(libs.anvil.compiler.api)
- implementation(libs.anvil.compiler.utils)
- implementation(libs.kotlinpoet)
- implementation(libs.dagger)
- implementation(libs.ksp.plugin)
- implementation(libs.kotlinpoet.ksp)
-}
diff --git a/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
deleted file mode 100644
index d5c111b0b2f..00000000000
--- a/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
+++ /dev/null
@@ -1 +0,0 @@
-io.element.android.anvilcodegen.ContributesNodeProcessorProvider
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f233620cdb6..4026888ee27 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +14,6 @@ import com.android.build.gradle.tasks.GenerateBuildConfig
import com.google.firebase.appdistribution.gradle.firebaseAppDistribution
import config.BuildTimeConfig
import extension.AssetCopyTask
-import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource
import extension.allEnterpriseImpl
@@ -23,8 +23,9 @@ import extension.allServicesImpl
import extension.buildConfigFieldStr
import extension.koverDependencies
import extension.locales
-import extension.setupAnvil
+import extension.setupDependencyInjection
import extension.setupKover
+import extension.testCommonDependencies
import java.util.Locale
plugins {
@@ -37,7 +38,7 @@ plugins {
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
// To be able to update the firebase.xml files, uncomment and build the project
- // id("com.google.gms.google-services")
+ // alias(libs.plugins.gms.google.services)
}
setupKover()
@@ -103,7 +104,8 @@ android {
}
val baseAppName = BuildTimeConfig.APPLICATION_NAME
- logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName)")
+ val buildType = if (isEnterpriseBuild) "Enterprise" else "FOSS"
+ logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]")
buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
@@ -196,6 +198,12 @@ android {
buildConfigFieldStr("FLAVOR_DESCRIPTION", "FDroid")
}
}
+
+ packaging {
+ resources.pickFirsts += setOf(
+ "META-INF/versions/9/OSGI-INF/MANIFEST.MF",
+ )
+ }
}
androidComponents {
@@ -247,11 +255,7 @@ knit {
}
}
-setupAnvil(
- generateDaggerCode = true,
- generateDaggerFactoriesUsingAnvil = false,
- componentMergingStrategy = ComponentMergingStrategy.KSP,
-)
+setupDependencyInjection()
dependencies {
allLibrariesImpl()
@@ -260,6 +264,7 @@ dependencies {
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
} else {
+ implementation(projects.features.enterprise.implFoss)
implementation(projects.appicon.element)
}
allFeaturesImpl(project)
@@ -293,12 +298,7 @@ dependencies {
implementation(libs.matrix.emojibase.bindings)
- testImplementation(libs.test.junit)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)
@@ -318,12 +318,14 @@ licensee {
allow("MIT")
allow("BSD-2-Clause")
allow("BSD-3-Clause")
+ allow("EPL-1.0")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")
allowUrl("https://jsoup.org/license")
allowUrl("https://asm.ow2.io/license.html")
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
+ allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE")
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 89dcab5fad7..c55b1bf1c29 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -42,3 +42,12 @@
}
-keep class io.element.android.x.di.** { *; }
+
+
+# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
+-keep class android.media.metrics.LogSessionId { *; }
+-keep class android.media.metrics.** { *; }
+
+# Keep Media3 classes that use reflection (https://github.com/androidx/media/issues/2535)
+-keep class androidx.media3.** { *; }
+-dontwarn android.media.metrics.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1fdc07d72ce..628c428790d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
+ android:exported="false"
+ tools:node="merge">
+
+
+
-
@@ -170,7 +172,7 @@
-
+
-
diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
index 30bef76339e..e29ff82d55f 100644
--- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
+++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,28 +10,31 @@ package io.element.android.x
import android.app.Application
import androidx.startup.AppInitializer
-import io.element.android.appconfig.RageshakeConfig
-import io.element.android.appconfig.isEnabled
-import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
-import io.element.android.libraries.di.DaggerComponentOwner
-import io.element.android.x.di.AppComponent
-import io.element.android.x.di.DaggerAppComponent
+import androidx.work.Configuration
+import dev.zacsweers.metro.createGraphFactory
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
+import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
+import io.element.android.x.di.AppGraph
import io.element.android.x.info.logApplicationInfo
+import io.element.android.x.initializer.CacheCleanerInitializer
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.PlatformInitializer
-class ElementXApplication : Application(), DaggerComponentOwner {
- override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this)
+class ElementXApplication : Application(), DependencyInjectionGraphOwner, Configuration.Provider {
+ override val graph: AppGraph = createGraphFactory().create(this)
+
+ override val workManagerConfiguration: Configuration = Configuration.Builder()
+ .setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
+ .build()
override fun onCreate() {
super.onCreate()
AppInitializer.getInstance(this).apply {
- if (RageshakeConfig.isEnabled) {
- initializeComponent(CrashInitializer::class.java)
- }
+ initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java)
}
+
logApplicationInfo(this)
}
}
diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt
index f80db878a76..162a55c3a7c 100644
--- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt
+++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,6 +17,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -25,6 +29,7 @@ import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
+import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
@@ -61,9 +66,13 @@ class MainActivity : NodeActivity() {
@Composable
private fun MainContent(appBindings: AppBindings) {
val migrationState = appBindings.migrationEntryPoint().present()
+ val colors by remember {
+ appBindings.enterpriseService().semanticColorsFlow(sessionId = null)
+ }.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appBindings.preferencesStore(),
- enterpriseService = appBindings.enterpriseService(),
+ compoundLight = colors.light,
+ compoundDark = colors.dark,
buildMeta = appBindings.buildMeta()
) {
CompositionLocalProvider(
diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt
index 2fd36db3b90..2004db8c3c3 100644
--- a/app/src/main/kotlin/io/element/android/x/MainNode.kt
+++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,8 +22,8 @@ import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.DaggerComponentOwner
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
+import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -38,8 +39,8 @@ class MainNode(
buildContext = buildContext,
plugins = plugins,
),
- DaggerComponentOwner {
- override val daggerComponent = (context as DaggerComponentOwner).daggerComponent
+ DependencyInjectionGraphOwner {
+ override val graph = (context as DependencyInjectionGraphOwner).graph
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
return createNode(buildContext = buildContext)
diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
index 8525f6356b2..6e157f6ac1a 100644
--- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
+++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
@@ -1,13 +1,15 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
-import com.squareup.anvil.annotations.ContributesTo
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
@@ -15,7 +17,6 @@ import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingService
diff --git a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt
deleted file mode 100644
index 2a0cac9d748..00000000000
--- a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.x.di
-
-import android.content.Context
-import com.squareup.anvil.annotations.MergeComponent
-import dagger.BindsInstance
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
-
-@SingleIn(AppScope::class)
-@MergeComponent(AppScope::class)
-interface AppComponent : NodeFactoriesBindings {
- @MergeComponent.Factory
- interface Factory {
- fun create(
- @ApplicationContext @BindsInstance
- context: Context
- ): AppComponent
- }
-}
diff --git a/app/src/main/kotlin/io/element/android/x/di/AppGraph.kt b/app/src/main/kotlin/io/element/android/x/di/AppGraph.kt
new file mode 100644
index 00000000000..7b5ba11f4c9
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/di/AppGraph.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.di
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.DependencyGraph
+import dev.zacsweers.metro.Multibinds
+import dev.zacsweers.metro.Provides
+import io.element.android.libraries.architecture.NodeFactoriesBindings
+import io.element.android.libraries.di.annotations.ApplicationContext
+import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
+import kotlin.reflect.KClass
+
+@DependencyGraph(AppScope::class)
+interface AppGraph : NodeFactoriesBindings {
+ val sessionGraphFactory: SessionGraph.Factory
+
+ @Multibinds
+ val workerProviders:
+ Map, MetroWorkerFactory.WorkerInstanceFactory<*>>
+
+ @DependencyGraph.Factory
+ interface Factory {
+ fun create(
+ @ApplicationContext @Provides
+ context: Context
+ ): AppGraph
+ }
+}
diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt
index c5bc3f4598b..87d0ece7a12 100644
--- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt
+++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,36 +12,37 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import androidx.preference.PreferenceManager
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Module
-import dagger.Provides
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.Provides
+import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.EnterpriseService
-import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
-import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
+import io.element.android.libraries.di.annotations.ApplicationContext
+import io.element.android.libraries.recentemojis.api.EmojibaseProvider
+import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig
import io.element.android.x.R
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
import java.io.File
-@Module
+@BindingContainer
@ContributesTo(AppScope::class)
object AppModule {
@Provides
+ @BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File {
return File(context.filesDir, "sessions")
}
@@ -105,11 +107,7 @@ object AppModule {
@Provides
@SingleIn(AppScope::class)
fun providesCoroutineDispatchers(): CoroutineDispatchers {
- return CoroutineDispatchers(
- io = Dispatchers.IO,
- computation = Dispatchers.Default,
- main = Dispatchers.Main,
- )
+ return CoroutineDispatchers.Default
}
@Provides
diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt
deleted file mode 100644
index fb7a24ae8a1..00000000000
--- a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.x.di
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.appnav.di.RoomComponentFactory
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.matrix.api.room.JoinedRoom
-import javax.inject.Inject
-
-@ContributesBinding(SessionScope::class)
-class DefaultRoomComponentFactory @Inject constructor(
- private val roomComponentBuilder: RoomComponent.Builder
-) : RoomComponentFactory {
- override fun create(room: JoinedRoom): Any {
- return roomComponentBuilder
- .joinedRoom(room)
- .baseRoom(room)
- .build()
- }
-}
diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt
new file mode 100644
index 00000000000..bc5c853c66c
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomGraphFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.di
+
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.appnav.di.RoomGraphFactory
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+
+@ContributesBinding(SessionScope::class)
+class DefaultRoomGraphFactory(
+ private val sessionGraph: SessionGraph,
+) : RoomGraphFactory {
+ override fun create(room: JoinedRoom): Any {
+ return sessionGraph.roomGraphFactory
+ .create(room, room)
+ }
+}
diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt
deleted file mode 100644
index 1c36991cd2f..00000000000
--- a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionComponentFactory.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.x.di
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.appnav.di.SessionComponentFactory
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.matrix.api.MatrixClient
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-class DefaultSessionComponentFactory @Inject constructor(
- private val sessionComponentBuilder: SessionComponent.Builder
-) : SessionComponentFactory {
- override fun create(client: MatrixClient): Any {
- return sessionComponentBuilder.client(client).build()
- }
-}
diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.kt
new file mode 100644
index 00000000000..9631713bb6a
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/di/DefaultSessionGraphFactory.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.di
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.appnav.di.SessionGraphFactory
+import io.element.android.libraries.matrix.api.MatrixClient
+
+@ContributesBinding(AppScope::class)
+class DefaultSessionGraphFactory(
+ private val appGraph: AppGraph
+) : SessionGraphFactory {
+ override fun create(client: MatrixClient): Any {
+ return appGraph.sessionGraphFactory.create(client)
+ }
+}
diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt
deleted file mode 100644
index ac126ce0f03..00000000000
--- a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.x.di
-
-import com.squareup.anvil.annotations.ContributesTo
-import com.squareup.anvil.annotations.MergeSubcomponent
-import dagger.BindsInstance
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.RoomScope
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
-import io.element.android.libraries.matrix.api.room.BaseRoom
-import io.element.android.libraries.matrix.api.room.JoinedRoom
-
-@SingleIn(RoomScope::class)
-@MergeSubcomponent(RoomScope::class)
-interface RoomComponent : NodeFactoriesBindings {
- @MergeSubcomponent.Builder
- interface Builder {
- @BindsInstance
- fun joinedRoom(room: JoinedRoom): Builder
-
- @BindsInstance
- fun baseRoom(room: BaseRoom): Builder
-
- fun build(): RoomComponent
- }
-
- @ContributesTo(SessionScope::class)
- interface ParentBindings {
- fun roomComponentBuilder(): Builder
- }
-}
diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt
new file mode 100644
index 00000000000..65883355075
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.di
+
+import dev.zacsweers.metro.GraphExtension
+import dev.zacsweers.metro.Provides
+import io.element.android.appnav.di.TimelineBindings
+import io.element.android.libraries.architecture.NodeFactoriesBindings
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.room.BaseRoom
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+
+@GraphExtension(RoomScope::class)
+interface RoomGraph : NodeFactoriesBindings, TimelineBindings {
+ @GraphExtension.Factory
+ interface Factory {
+ fun create(
+ @Provides joinedRoom: JoinedRoom,
+ @Provides baseRoom: BaseRoom
+ ): RoomGraph
+ }
+}
diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt
deleted file mode 100644
index 7cdc6869174..00000000000
--- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.x.di
-
-import com.squareup.anvil.annotations.ContributesTo
-import com.squareup.anvil.annotations.MergeSubcomponent
-import dagger.BindsInstance
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
-import io.element.android.libraries.matrix.api.MatrixClient
-
-@SingleIn(SessionScope::class)
-@MergeSubcomponent(SessionScope::class)
-interface SessionComponent : NodeFactoriesBindings {
- @MergeSubcomponent.Builder
- interface Builder {
- @BindsInstance
- fun client(matrixClient: MatrixClient): Builder
-
- fun build(): SessionComponent
- }
-
- @ContributesTo(AppScope::class)
- interface ParentBindings {
- fun sessionComponentBuilder(): Builder
- }
-}
diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt b/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt
new file mode 100644
index 00000000000..e53d0eb56d8
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/di/SessionGraph.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.di
+
+import dev.zacsweers.metro.GraphExtension
+import dev.zacsweers.metro.Provides
+import io.element.android.libraries.architecture.NodeFactoriesBindings
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+
+@GraphExtension(SessionScope::class)
+interface SessionGraph : NodeFactoriesBindings {
+ val roomGraphFactory: RoomGraph.Factory
+
+ @GraphExtension.Factory
+ interface Factory {
+ fun create(@Provides matrixClient: MatrixClient): SessionGraph
+ }
+}
diff --git a/app/src/main/kotlin/io/element/android/x/info/Logs.kt b/app/src/main/kotlin/io/element/android/x/info/Logs.kt
index cca271e199f..2b66fef55ec 100644
--- a/app/src/main/kotlin/io/element/android/x/info/Logs.kt
+++ b/app/src/main/kotlin/io/element/android/x/info/Logs.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CacheCleanerInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CacheCleanerInitializer.kt
new file mode 100644
index 00000000000..3e187bcafa7
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/initializer/CacheCleanerInitializer.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.x.initializer
+
+import android.content.Context
+import androidx.startup.Initializer
+import io.element.android.features.cachecleaner.impl.CacheCleanerBindings
+import io.element.android.libraries.architecture.bindings
+
+class CacheCleanerInitializer : Initializer {
+ override fun create(context: Context) {
+ context.bindings().cacheCleaner().clearCache()
+ }
+
+ override fun dependencies(): List>> = emptyList()
+}
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
index 72e864bbeae..3caff8af096 100644
--- a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
+++ b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,10 +11,14 @@ package io.element.android.x.initializer
import android.content.Context
import androidx.startup.Initializer
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
+import io.element.android.features.rageshake.impl.di.RageshakeBindings
+import io.element.android.libraries.architecture.bindings
class CrashInitializer : Initializer {
override fun create(context: Context) {
- VectorUncaughtExceptionHandler(context).activate()
+ VectorUncaughtExceptionHandler(
+ context.bindings().preferencesCrashDataStore(),
+ ).activate()
}
override fun dependencies(): List>> = emptyList()
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt
index 6dd0d69dc04..0eea5123b40 100644
--- a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt
+++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,11 +11,10 @@ package io.element.android.x.initializer
import android.content.Context
import android.system.Os
import androidx.startup.Initializer
-import io.element.android.features.rageshake.api.reporter.BugReporter
+import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
-import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.x.di.AppBindings
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
@@ -34,7 +34,7 @@ class PlatformInitializer : Initializer {
val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() }
val tracingConfiguration = TracingConfiguration(
writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) },
- writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter),
+ writesToFilesConfiguration = bugReporter.createWriteToFilesConfiguration(),
logLevel = logLevel,
extraTargets = listOf(ELEMENT_X_TARGET),
traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() },
@@ -45,14 +45,5 @@ class PlatformInitializer : Initializer {
Os.setenv("RUST_BACKTRACE", "1", true)
}
- private fun defaultWriteToDiskConfiguration(bugReporter: BugReporter): WriteToFilesConfiguration.Enabled {
- return WriteToFilesConfiguration.Enabled(
- directory = bugReporter.logDirectory().absolutePath,
- filenamePrefix = "logs",
- // Keep a maximum of 1 week of log files.
- numberOfFiles = 7 * 24,
- )
- }
-
override fun dependencies(): List>> = mutableListOf()
}
diff --git a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
index fedcdf2919e..204ee273133 100644
--- a/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
+++ b/app/src/main/kotlin/io/element/android/x/intent/DefaultIntentProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,19 +11,19 @@ package io.element.android.x.intent
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.deeplink.DeepLinkCreator
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.libraries.deeplink.api.DeepLinkCreator
+import io.element.android.libraries.di.annotations.ApplicationContext
+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.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultIntentProvider @Inject constructor(
+class DefaultIntentProvider(
@ApplicationContext private val context: Context,
private val deepLinkCreator: DeepLinkCreator,
) : IntentProvider {
@@ -30,10 +31,11 @@ class DefaultIntentProvider @Inject constructor(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
+ eventId: EventId?,
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
- data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
+ data = deepLinkCreator.create(sessionId, roomId, threadId, eventId).toUri()
}
}
}
diff --git a/app/src/main/kotlin/io/element/android/x/intent/SafeUriHandler.kt b/app/src/main/kotlin/io/element/android/x/intent/SafeUriHandler.kt
index d225b8ea346..1e9af37c585 100644
--- a/app/src/main/kotlin/io/element/android/x/intent/SafeUriHandler.kt
+++ b/app/src/main/kotlin/io/element/android/x/intent/SafeUriHandler.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt
index 4a103b4daa1..ad4f9a47b2a 100644
--- a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt
+++ b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt
@@ -1,21 +1,21 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.oidc
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.di.AppScope
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.x.R
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultOidcRedirectUrlProvider @Inject constructor(
+class DefaultOidcRedirectUrlProvider(
private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider {
override fun provide() = buildString {
diff --git a/app/src/main/res/drawable/transparent.xml b/app/src/main/res/drawable/transparent.xml
index f5f4d3a7bfd..248ccbce22c 100644
--- a/app/src/main/res/drawable/transparent.xml
+++ b/app/src/main/res/drawable/transparent.xml
@@ -1,7 +1,8 @@
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
index b55c556736c..8cbf8eaf6ac 100644
--- a/app/src/main/res/resources.properties
+++ b/app/src/main/res/resources.properties
@@ -1,6 +1,7 @@
+# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2024 New Vector Ltd.
#
-# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
# Please see LICENSE files in the repository root for full details.
unqualifiedResLocale=en
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index c4014b842c0..af159a0b117 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,7 +1,8 @@
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f61fb338683..d666caddeab 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,7 +1,8 @@
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 76fb597109a..4190fd84f2d 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,14 +1,17 @@
-
+
-
+
diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml
index 745752ed0be..c4666928558 100644
--- a/app/src/main/res/xml/automotive_app_desc.xml
+++ b/app/src/main/res/xml/automotive_app_desc.xml
@@ -1,7 +1,8 @@
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
index 3602e38d2db..06db4ae4cbb 100644
--- a/app/src/main/res/xml/backup_rules.xml
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -1,7 +1,8 @@
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index 73a2ed405cf..a92d7b2ef91 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
index 5e81d6b9643..4598a74e51f 100644
--- a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
+++ b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt
@@ -1,19 +1,29 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
+@file:Suppress("SameParameterValue")
+
package io.element.android.x.intent
import android.content.Context
import android.content.Intent
import com.google.common.truth.Truth.assertThat
-import io.element.android.libraries.deeplink.DeepLinkCreator
+import io.element.android.libraries.deeplink.api.DeepLinkCreator
+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.SessionId
+import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.test.AN_EVENT_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_THREAD_ID
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
import io.element.android.x.MainActivity
import org.junit.Test
import org.junit.runner.RunWith
@@ -23,45 +33,33 @@ import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class DefaultIntentProviderTest {
@Test
- fun `test getViewRoomIntent with Session`() {
- val sut = createDefaultIntentProvider()
- val result = sut.getViewRoomIntent(
- sessionId = A_SESSION_ID,
- roomId = null,
- threadId = null,
- )
- result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org")
- }
-
- @Test
- fun `test getViewRoomIntent with Session and Room`() {
- val sut = createDefaultIntentProvider()
- val result = sut.getViewRoomIntent(
- sessionId = A_SESSION_ID,
- roomId = A_ROOM_ID,
- threadId = null,
+ fun `test getViewRoomIntent with data`() {
+ val deepLinkCreator = lambdaRecorder { _, _, _, _ -> "deepLinkCreatorResult" }
+ val sut = createDefaultIntentProvider(
+ deepLinkCreator = { sessionId, roomId, threadId, eventId -> deepLinkCreator.invoke(sessionId, roomId, threadId, eventId) },
)
- result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
- }
-
- @Test
- fun `test getViewRoomIntent with Session, Room and Thread`() {
- val sut = createDefaultIntentProvider()
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
+ eventId = AN_EVENT_ID,
)
result.commonAssertions()
- assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
+ assertThat(result.data.toString()).isEqualTo("deepLinkCreatorResult")
+ deepLinkCreator.assertions().isCalledOnce().with(
+ value(A_SESSION_ID),
+ value(A_ROOM_ID),
+ value(A_THREAD_ID),
+ value(AN_EVENT_ID),
+ )
}
- private fun createDefaultIntentProvider(): DefaultIntentProvider {
+ private fun createDefaultIntentProvider(
+ deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _, _ -> "" },
+ ): DefaultIntentProvider {
return DefaultIntentProvider(
context = RuntimeEnvironment.getApplication() as Context,
- deepLinkCreator = DeepLinkCreator(),
+ deepLinkCreator = deepLinkCreator,
)
}
diff --git a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt
index 5a0df57bdad..18567355d2f 100644
--- a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt
+++ b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts
index 15b8bd51ba4..45496acb770 100644
--- a/appconfig/build.gradle.kts
+++ b/appconfig/build.gradle.kts
@@ -2,9 +2,10 @@ import config.BuildTimeConfig
import extension.buildConfigFieldStr
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
@@ -32,7 +33,7 @@ android {
value = if (isEnterpriseBuild) {
BuildTimeConfig.BUG_REPORT_URL ?: ""
} else {
- "https://riot.im/bugreports/submit"
+ "https://rageshakes.element.io/api/submit"
},
)
buildConfigFieldStr(
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt
index 346fce47254..e32e4828423 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ApplicationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ApplicationConfig.kt
index eeeb52f0957..e1ae6896bb9 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/ApplicationConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ApplicationConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt
index 50612909489..7432a6904fd 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
index ca9a6391f2a..ea4c2640fe4 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt
index 9dee73fc94d..c0c152142e1 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
index c3dbe2bea53..f2ef0bc42f4 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt
index a8f424bd085..76b96bf398a 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/MessageComposerConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/MessageComposerConfig.kt
index 68e5a8b6adc..f3893f0f61c 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/MessageComposerConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/MessageComposerConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
index 41acf5d2664..cac2f8a8ed7 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/NotificationConfig.kt
@@ -1,14 +1,15 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
-import android.graphics.Color
import androidx.annotation.ColorInt
+import androidx.core.graphics.toColorInt
object NotificationConfig {
/**
@@ -27,5 +28,5 @@ object NotificationConfig {
const val SHOW_QUICK_REPLY_ACTION = true
@ColorInt
- val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
+ val NOTIFICATION_ACCENT_COLOR: Int = "#FF0DBD8B".toColorInt()
}
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
index c84cc273bba..c59f5d19fee 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
@@ -1,16 +1,14 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
object OnBoardingConfig {
- /** Whether the user can use QR code login. */
- const val CAN_LOGIN_WITH_QR_CODE = true
-
/** Whether the user can create an account using the app. */
const val CAN_CREATE_ACCOUNT = true
}
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt
index 506b17f7a89..d36fb742b07 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt
index 8c836bc8a2e..1f6609ecc33 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -25,9 +26,3 @@ object RageshakeConfig {
*/
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
}
-
-/**
- * Whether the rageshake feature is enabled.
- */
-val RageshakeConfig.isEnabled: Boolean
- get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty()
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RoomListConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RoomListConfig.kt
index dd2a22a5ab4..7369f1393c8 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/RoomListConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RoomListConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt
index 7e38ec80923..539b67825d5 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/VoiceMessageConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/VoiceMessageConfig.kt
index ca2b99e633f..b77b2dc1136 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/VoiceMessageConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/VoiceMessageConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appicon/element/build.gradle.kts b/appicon/element/build.gradle.kts
index fd5a2d74ca3..23c21d4863c 100644
--- a/appicon/element/build.gradle.kts
+++ b/appicon/element/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt b/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt
index c25675bffcb..bffcdaecb36 100644
--- a/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt
+++ b/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 777a587a502..9de193bab34 100644
--- a/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,7 +1,8 @@
diff --git a/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 777a587a502..9de193bab34 100644
--- a/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/appicon/element/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,7 +1,8 @@
diff --git a/appicon/enterprise/build.gradle.kts b/appicon/enterprise/build.gradle.kts
index 462397b5b24..bc5cdea0b86 100644
--- a/appicon/enterprise/build.gradle.kts
+++ b/appicon/enterprise/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/appicon/enterprise/src/main/kotlin/io/element/android/appicon/enterprise/IconPreview.kt b/appicon/enterprise/src/main/kotlin/io/element/android/appicon/enterprise/IconPreview.kt
index b3f96785f49..70e02012188 100644
--- a/appicon/enterprise/src/main/kotlin/io/element/android/appicon/enterprise/IconPreview.kt
+++ b/appicon/enterprise/src/main/kotlin/io/element/android/appicon/enterprise/IconPreview.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher.xml b/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher.xml
index 4806e4bffd2..38ef234b879 100644
--- a/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ b/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -1,7 +1,8 @@
diff --git a/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher_round.xml
index 11c51fdf6a3..0fd0f91c199 100644
--- a/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ b/appicon/enterprise/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -1,7 +1,8 @@
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 09b0a634a44..aa0bc047722 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -1,14 +1,16 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("UnstableApiUsage")
import extension.allFeaturesApi
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
plugins {
id("io.element.android-compose-library")
@@ -19,15 +21,17 @@ android {
namespace = "io.element.android.appnav"
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
allFeaturesApi(project)
implementation(projects.libraries.core)
+ implementation(projects.libraries.accountselect.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
+ implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oidc.api)
implementation(projects.libraries.preferences.api)
@@ -35,36 +39,33 @@ dependencies {
implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.matrixmedia.api)
+ implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
+ implementation(projects.features.login.api)
implementation(libs.coil)
+ implementation(projects.features.announcement.api)
implementation(projects.features.ftue.api)
implementation(projects.features.share.api)
- implementation(projects.features.viewfolder.api)
implementation(projects.services.apperror.impl)
implementation(projects.services.appnavstate.api)
implementation(projects.services.analytics.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
+ testImplementation(projects.features.forward.test)
testImplementation(projects.features.networkmonitor.test)
- testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test)
- testImplementation(projects.features.share.test)
+ testImplementation(projects.services.appnavstate.impl)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
- testImplementation(libs.test.appyx.junit)
- testImplementation(libs.test.arch.core)
+ testImplementation(projects.services.toolbox.test)
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt
index 2f1e4e75ea7..1185016c0bf 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
index ae169fcd347..0dbcb0f3f11 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,30 +22,31 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.appnav.di.SessionComponentFactory
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.DaggerComponentOwner
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import kotlinx.parcelize.Parcelize
/**
- * `LoggedInAppScopeFlowNode` is a Node responsible to set up the Dagger
+ * `LoggedInAppScopeFlowNode` is a Node responsible to set up the Session graph.
* [io.element.android.libraries.di.SessionScope]. It has only one child: [LoggedInFlowNode].
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
*/
@ContributesNode(AppScope::class)
-class LoggedInAppScopeFlowNode @AssistedInject constructor(
+@AssistedInject
+class LoggedInAppScopeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- sessionComponentFactory: SessionComponentFactory,
+ sessionGraphFactory: SessionGraphFactory,
private val imageLoaderHolder: ImageLoaderHolder,
) : ParentNode(
navModel = PermanentNavModel(
@@ -53,11 +55,14 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
),
buildContext = buildContext,
plugins = plugins
-), DaggerComponentOwner {
+), DependencyInjectionGraphOwner {
interface Callback : Plugin {
- fun onOpenBugReport()
+ fun navigateToBugReport()
+ fun navigateToAddAccount()
}
+ private val callback: Callback = callback()
+
@Parcelize
object NavTarget : Parcelable
@@ -66,12 +71,12 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
) : NodeInputs
private val inputs: Inputs = inputs()
- override val daggerComponent = sessionComponentFactory.create(inputs.matrixClient)
+ override val graph = sessionGraphFactory.create(inputs.matrixClient)
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
- onCreate = {
+ onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get(inputs.matrixClient))
},
)
@@ -79,8 +84,12 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
val callback = object : LoggedInFlowNode.Callback {
- override fun onOpenBugReport() {
- plugins().forEach { it.onOpenBugReport() }
+ override fun navigateToBugReport() {
+ callback.navigateToBugReport()
+ }
+
+ override fun navigateToAddAccount() {
+ callback.navigateToAddAccount()
}
}
return createNode(buildContext, listOf(callback))
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
index 65980e34275..cee2296d435 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
@@ -1,12 +1,14 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav
+import dev.zacsweers.metro.Inject
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -14,12 +16,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import javax.inject.Inject
-class LoggedInEventProcessor @Inject constructor(
+@Inject
+class LoggedInEventProcessor(
private val snackbarDispatcher: SnackbarDispatcher,
private val roomMembershipObserver: RoomMembershipObserver,
) {
@@ -28,9 +31,18 @@ class LoggedInEventProcessor @Inject constructor(
fun observeEvents(coroutineScope: CoroutineScope) {
observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom }
- .onEach {
- when (it.change) {
- MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
+ .distinctUntilChanged()
+ .onEach { roomMemberShipUpdate ->
+ when (roomMemberShipUpdate.change) {
+ MembershipChange.LEFT -> {
+ displayMessage(
+ if (roomMemberShipUpdate.isSpace) {
+ CommonStrings.common_current_user_left_space
+ } else {
+ CommonStrings.common_current_user_left_room
+ }
+ )
+ }
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
else -> Unit
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index 043dc75a92d..5d0f373c111 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.PermanentChild
@@ -23,7 +25,6 @@ import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE
import com.bumble.appyx.navmodel.backstack.BackStack.State.CREATED
@@ -36,35 +37,43 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.loggedin.MediaPreviewConfigMigration
import io.element.android.appnav.loggedin.SendQueues
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
-import io.element.android.features.createroom.api.CreateRoomEntryPoint
+import io.element.android.compound.colors.SemanticColorsLightDark
+import io.element.android.features.enterprise.api.EnterpriseService
+import io.element.android.features.enterprise.api.SessionEnterpriseService
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.FtueState
import io.element.android.features.home.api.HomeEntryPoint
-import io.element.android.features.logout.api.LogoutEntryPoint
+import io.element.android.features.networkmonitor.api.NetworkMonitor
+import io.element.android.features.networkmonitor.api.NetworkStatus
+import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
+import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.callback
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.core.meta.BuildMeta
+import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
@@ -72,12 +81,17 @@ 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.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-import io.element.android.libraries.matrix.api.core.SessionId
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.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
+import io.element.android.libraries.preferences.api.store.AppPreferencesStore
+import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
+import io.element.android.libraries.ui.common.nodes.emptyNode
+import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
+import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
@@ -91,26 +105,18 @@ import java.time.Duration
import java.time.Instant
import java.util.Optional
import java.util.UUID
-import kotlin.collections.List
-import kotlin.collections.any
-import kotlin.collections.emptyList
-import kotlin.collections.first
-import kotlin.collections.forEach
-import kotlin.collections.listOf
-import kotlin.collections.mapNotNull
-import kotlin.collections.plus
-import kotlin.collections.setOf
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
@ContributesNode(SessionScope::class)
-class LoggedInFlowNode @AssistedInject constructor(
+@AssistedInject
+class LoggedInFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val homeEntryPoint: HomeEntryPoint,
private val preferencesEntryPoint: PreferencesEntryPoint,
- private val createRoomEntryPoint: CreateRoomEntryPoint,
+ private val startChatEntryPoint: StartChatEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
@@ -122,10 +128,17 @@ class LoggedInFlowNode @AssistedInject constructor(
private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
- private val logoutEntryPoint: LogoutEntryPoint,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
+ private val sessionEnterpriseService: SessionEnterpriseService,
+ private val networkMonitor: NetworkMonitor,
+ private val notificationConversationService: NotificationConversationService,
+ private val syncService: SyncService,
+ private val enterpriseService: EnterpriseService,
+ private val appPreferencesStore: AppPreferencesStore,
+ private val buildMeta: BuildMeta,
snackbarDispatcher: SnackbarDispatcher,
+ private val analyticsService: AnalyticsService,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -139,12 +152,14 @@ class LoggedInFlowNode @AssistedInject constructor(
plugins = plugins
) {
interface Callback : Plugin {
- fun onOpenBugReport()
+ fun navigateToBugReport()
+ fun navigateToAddAccount()
}
+ private val callback: Callback = callback()
private val loggedInFlowProcessor = LoggedInEventProcessor(
- snackbarDispatcher,
- matrixClient.roomMembershipObserver(),
+ snackbarDispatcher = snackbarDispatcher,
+ roomMembershipObserver = matrixClient.roomMembershipObserver,
)
private val verificationListener = object : SessionVerificationServiceListener {
@@ -182,16 +197,26 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
-
+ lifecycleScope.launch {
+ sessionEnterpriseService.init()
+ }
lifecycle.subscribe(
onCreate = {
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
- matrixClient.sessionVerificationService().setListener(verificationListener)
+ matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
+ sessionCoroutineScope.launch {
+ // Wait for the network to be connected before pre-fetching the max file upload size
+ networkMonitor.connectivity.first { networkStatus -> networkStatus == NetworkStatus.Connected }
+ matrixClient.getMaxFileUploadSize()
+ }
+
+ analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
+
ftueService.state
.onEach { ftueState ->
when (ftueState) {
@@ -202,11 +227,17 @@ class LoggedInFlowNode @AssistedInject constructor(
}
.launchIn(lifecycleScope)
},
+ onResume = {
+ lifecycleScope.launch {
+ val availableRoomIds = matrixClient.getJoinedRoomIds().getOrNull() ?: return@launch
+ notificationConversationService.onAvailableRoomsChanged(sessionId = matrixClient.sessionId, roomIds = availableRoomIds)
+ }
+ },
onDestroy = {
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
- matrixClient.sessionVerificationService().setListener(null)
+ matrixClient.sessionVerificationService.setListener(null)
}
)
setupSendingQueue()
@@ -232,7 +263,7 @@ class LoggedInFlowNode @AssistedInject constructor(
val serverNames: List = emptyList(),
val trigger: JoinedRoom.Trigger? = null,
val roomDescription: RoomDescription? = null,
- val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(),
+ val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
val targetId: UUID = UUID.randomUUID(),
) : NavTarget
@@ -258,21 +289,18 @@ class LoggedInFlowNode @AssistedInject constructor(
data object Ftue : NavTarget
@Parcelize
- data object RoomDirectorySearch : NavTarget
+ data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val intent: Intent) : NavTarget
- @Parcelize
- data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
-
@Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
- NavTarget.Placeholder -> createNode(buildContext)
+ NavTarget.Placeholder -> emptyNode(buildContext)
NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() {
@@ -283,54 +311,47 @@ class LoggedInFlowNode @AssistedInject constructor(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
- override fun onRoomClick(roomId: RoomId) {
+ override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
- override fun onSettingsClick() {
+ override fun navigateToSettings() {
backstack.push(NavTarget.Settings())
}
- override fun onCreateRoomClick() {
+ override fun navigateToCreateRoom() {
backstack.push(NavTarget.CreateRoom)
}
- override fun onSetUpRecoveryClick() {
+ override fun navigateToSetUpRecovery() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
}
- override fun onSessionConfirmRecoveryKeyClick() {
+ override fun navigateToEnterRecoveryKey() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
}
- override fun onRoomSettingsClick(roomId: RoomId) {
+ override fun navigateToRoomSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
}
- override fun onReportBugClick() {
- plugins().forEach { it.onOpenBugReport() }
- }
-
- override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
- backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
+ override fun navigateToBugReport() {
+ callback.navigateToBugReport()
}
}
- homeEntryPoint
- .nodeBuilder(this, buildContext)
- .callback(callback)
- .build()
+ homeEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ callback = callback,
+ )
}
is NavTarget.Room -> {
- val callback = object : JoinedRoomLoadedFlowNode.Callback {
- override fun onOpenRoom(roomId: RoomId) {
- backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
- }
-
- override fun onForwardedToSingleRoom(roomId: RoomId) {
- sessionCoroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) }
+ val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
+ override fun navigateToRoom(roomId: RoomId, serverNames: List) {
+ backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
}
- override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
+ override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
when (data) {
is PermalinkData.UserLink -> {
// Should not happen (handled by MessagesNode)
@@ -341,7 +362,7 @@ class LoggedInFlowNode @AssistedInject constructor(
roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoom.Trigger.Timeline,
- initialElement = RoomNavigationTarget.Messages(data.eventId),
+ initialElement = RoomNavigationTarget.Root(data.eventId),
)
if (pushToBackstack) {
backstack.push(target)
@@ -356,7 +377,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
- override fun onOpenGlobalNotificationSettings() {
+ override fun navigateToGlobalNotificationSettings() {
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
}
}
@@ -367,81 +388,89 @@ class LoggedInFlowNode @AssistedInject constructor(
trigger = Optional.ofNullable(navTarget.trigger),
initialElement = navTarget.initialElement
)
- createNode(buildContext, plugins = listOf(inputs, callback))
+ createNode(buildContext, plugins = listOf(inputs, joinedRoomCallback))
}
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {
- override fun onOpenRoom(roomId: RoomId) {
+ override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
- userProfileEntryPoint.nodeBuilder(this, buildContext)
- .params(UserProfileEntryPoint.Params(userId = navTarget.userId))
- .callback(callback)
- .build()
+ userProfileEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = UserProfileEntryPoint.Params(userId = navTarget.userId),
+ callback = callback,
+ )
}
is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {
- override fun onOpenBugReport() {
- plugins().forEach { it.onOpenBugReport() }
+ override fun navigateToAddAccount() {
+ callback.navigateToAddAccount()
+ }
+
+ override fun navigateToBugReport() {
+ callback.navigateToBugReport()
}
- override fun onSecureBackupClick() {
+ override fun navigateToSecureBackup() {
backstack.push(NavTarget.SecureBackup())
}
- override fun onOpenRoomNotificationSettings(roomId: RoomId) {
+ override fun navigateToRoomNotificationSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
}
- override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
- // We do not check the sessionId, but it will have to be done at some point (multi account)
- if (sessionId != matrixClient.sessionId) {
- Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId")
- }
- backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
+ override fun navigateToEvent(roomId: RoomId, eventId: EventId) {
+ backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId)))
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
- preferencesEntryPoint.nodeBuilder(this, buildContext)
- .params(inputs)
- .callback(callback)
- .build()
+ preferencesEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = inputs,
+ callback = callback,
+ )
}
NavTarget.CreateRoom -> {
- val callback = object : CreateRoomEntryPoint.Callback {
- override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) {
+ val callback = object : StartChatEntryPoint.Callback {
+ override fun onRoomCreated(roomIdOrAlias: RoomIdOrAlias, serverNames: List) {
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
}
- override fun onOpenRoomDirectory() {
- backstack.push(NavTarget.RoomDirectorySearch)
+ override fun navigateToRoomDirectory() {
+ backstack.push(NavTarget.RoomDirectory)
}
}
- createRoomEntryPoint
- .nodeBuilder(this, buildContext)
- .callback(callback)
- .build()
+ startChatEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ callback = callback,
+ )
}
is NavTarget.SecureBackup -> {
- secureBackupEntryPoint.nodeBuilder(this, buildContext)
- .params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
- .callback(object : SecureBackupEntryPoint.Callback {
+ secureBackupEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement),
+ callback = object : SecureBackupEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
- })
- .build()
+ },
+ )
}
NavTarget.Ftue -> {
- ftueEntryPoint.nodeBuilder(this, buildContext)
- .build()
+ ftueEntryPoint.createNode(this, buildContext)
}
- NavTarget.RoomDirectorySearch -> {
- roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
- .callback(object : RoomDirectoryEntryPoint.Callback {
- override fun onResultClick(roomDescription: RoomDescription) {
+ NavTarget.RoomDirectory -> {
+ roomDirectoryEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ callback = object : RoomDirectoryEntryPoint.Callback {
+ override fun navigateToRoom(roomDescription: RoomDescription) {
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
@@ -450,43 +479,35 @@ class LoggedInFlowNode @AssistedInject constructor(
)
)
}
- })
- .build()
+ },
+ )
}
is NavTarget.IncomingShare -> {
- shareEntryPoint.nodeBuilder(this, buildContext)
- .callback(object : ShareEntryPoint.Callback {
+ shareEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = ShareEntryPoint.Params(intent = navTarget.intent),
+ callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List) {
- navigateUp()
- if (roomIds.size == 1) {
- val targetRoomId = roomIds.first()
- backstack.push(NavTarget.Room(targetRoomId.toRoomIdOrAlias()))
+ backstack.pop()
+ roomIds.singleOrNull()?.let { roomId ->
+ backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
- })
- .params(ShareEntryPoint.Params(intent = navTarget.intent))
- .build()
- }
- is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> {
- val callback = object : LogoutEntryPoint.Callback {
- override fun onChangeRecoveryKeyClick() {
- backstack.push(NavTarget.SecureBackup())
- }
- }
-
- logoutEntryPoint.nodeBuilder(this, buildContext)
- .callback(callback)
- .build()
+ },
+ )
}
is NavTarget.IncomingVerificationRequest -> {
- incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
- .params(IncomingVerificationEntryPoint.Params(navTarget.data))
- .callback(object : IncomingVerificationEntryPoint.Callback {
+ incomingVerificationEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = IncomingVerificationEntryPoint.Params(navTarget.data),
+ callback = object : IncomingVerificationEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
- })
- .build()
+ },
+ )
}
}
}
@@ -497,7 +518,7 @@ class LoggedInFlowNode @AssistedInject constructor(
trigger: JoinedRoom.Trigger? = null,
eventId: EventId? = null,
clearBackstack: Boolean,
- ) {
+ ): RoomFlowNode {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
@@ -506,12 +527,19 @@ class LoggedInFlowNode @AssistedInject constructor(
roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames,
trigger = trigger,
- initialElement = RoomNavigationTarget.Messages(
- focusedEventId = eventId
- )
+ initialElement = RoomNavigationTarget.Root(eventId = eventId)
)
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
}
+
+ // If we don't do this check, we might be returning while a previous node with the same type is still displayed
+ // This means we may attach some new nodes to that one, which will be quickly replaced by the one instantiated above
+ return waitForChildAttached {
+ it is NavTarget.Room &&
+ it.roomIdOrAlias == roomIdOrAlias &&
+ it.initialElement is RoomNavigationTarget.Root &&
+ it.initialElement.eventId == eventId
+ }
}
suspend fun attachUser(userId: UserId) {
@@ -540,20 +568,30 @@ class LoggedInFlowNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
- Box(modifier = modifier) {
- val ftueState by ftueService.state.collectAsState()
- BackstackView()
- if (ftueState is FtueState.Complete) {
- PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
+ val colors by remember {
+ enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
+ }.collectAsState(SemanticColorsLightDark.default)
+ ElementThemeApp(
+ appPreferencesStore = appPreferencesStore,
+ compoundLight = colors.light,
+ compoundDark = colors.dark,
+ buildMeta = buildMeta,
+ ) {
+ val isOnline by syncService.isOnline.collectAsState()
+ ConnectivityIndicatorContainer(
+ isOnline = isOnline,
+ modifier = modifier,
+ ) { contentModifier ->
+ Box(modifier = contentModifier) {
+ val ftueState by ftueService.state.collectAsState()
+ BackstackView()
+ if (ftueState is FtueState.Complete) {
+ PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
+ }
+ }
}
}
}
-
- @ContributesNode(AppScope::class)
- class PlaceholderNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- ) : Node(buildContext, plugins = plugins)
}
@Parcelize
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
index bd4802b77ba..2544abbfb34 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -18,29 +19,30 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory
+import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-class NotLoggedInFlowNode @AssistedInject constructor(
+@AssistedInject
+class NotLoggedInFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val loginEntryPoint: LoginEntryPoint,
- private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
+ private val imageLoaderHolder: ImageLoaderHolder,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -54,16 +56,17 @@ class NotLoggedInFlowNode @AssistedInject constructor(
) : NodeInputs
interface Callback : Plugin {
- fun onOpenBugReport()
+ fun navigateToBugReport()
}
+ private val callback: Callback = callback()
private val inputs = inputs()
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
- onCreate = {
- SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader())
+ onResume = {
+ SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
},
)
}
@@ -77,20 +80,19 @@ class NotLoggedInFlowNode @AssistedInject constructor(
return when (navTarget) {
NavTarget.Root -> {
val callback = object : LoginEntryPoint.Callback {
- override fun onReportProblem() {
- plugins().forEach { it.onOpenBugReport() }
+ override fun navigateToBugReport() {
+ callback.navigateToBugReport()
}
}
- loginEntryPoint
- .nodeBuilder(this, buildContext)
- .params(
- LoginEntryPoint.Params(
- accountProvider = inputs.loginParams?.accountProvider,
- loginHint = inputs.loginParams?.loginHint,
- )
- )
- .callback(callback)
- .build()
+ loginEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = LoginEntryPoint.Params(
+ accountProvider = inputs.loginParams?.accountProvider,
+ loginHint = inputs.loginParams?.loginHint,
+ ),
+ callback = callback,
+ )
}
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index ae9f935061c..a0d2f44640a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,70 +10,82 @@ package io.element.android.appnav
import android.content.Intent
import android.os.Parcelable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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 com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
+import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
+import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
-import io.element.android.features.enterprise.api.EnterpriseService
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
+import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.signedout.api.SignedOutEntryPoint
-import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
+import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol
-import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.deeplink.api.DeeplinkData
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState
+import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class)
-class RootFlowNode @AssistedInject constructor(
+@AssistedInject
+class RootFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
- private val authenticationService: MatrixAuthenticationService,
- private val enterpriseService: EnterpriseService,
+ private val sessionStore: SessionStore,
+ private val accountProviderAccessControl: AccountProviderAccessControl,
private val navStateFlowFactory: RootNavStateFlowFactory,
private val matrixSessionCache: MatrixSessionCache,
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
- private val viewFolderEntryPoint: ViewFolderEntryPoint,
private val signedOutEntryPoint: SignedOutEntryPoint,
+ private val accountSelectEntryPoint: AccountSelectEntryPoint,
private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow,
+ private val featureFlagService: FeatureFlagService,
+ private val announcementService: AnnouncementService,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.SplashScreen,
@@ -94,27 +107,24 @@ class RootFlowNode @AssistedInject constructor(
}
private fun observeNavState() {
- navStateFlowFactory.create(buildContext.savedStateMap)
- .distinctUntilChanged()
- .onEach { navState ->
- Timber.v("navState=$navState")
- when (navState.loggedInState) {
- is LoggedInState.LoggedIn -> {
- if (navState.loggedInState.isTokenValid) {
- tryToRestoreLatestSession(
- onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
- onFailure = { switchToNotLoggedInFlow(null) }
- )
- } else {
- switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
- }
- }
- LoggedInState.NotLoggedIn -> {
- switchToNotLoggedInFlow(null)
+ navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState ->
+ Timber.v("navState=$navState")
+ when (navState.loggedInState) {
+ is LoggedInState.LoggedIn -> {
+ if (navState.loggedInState.isTokenValid) {
+ tryToRestoreLatestSession(
+ onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
+ onFailure = { switchToNotLoggedInFlow(null) }
+ )
+ } else {
+ switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
}
}
+ LoggedInState.NotLoggedIn -> {
+ switchToNotLoggedInFlow(null)
+ }
}
- .launchIn(lifecycleScope)
+ }.launchIn(lifecycleScope)
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
@@ -135,22 +145,19 @@ class RootFlowNode @AssistedInject constructor(
onFailure: () -> Unit,
onSuccess: (SessionId) -> Unit,
) {
- matrixSessionCache.getOrRestore(sessionId)
- .onSuccess {
- Timber.v("Succeed to restore session $sessionId")
- onSuccess(sessionId)
- }
- .onFailure {
- Timber.e(it, "Failed to restore session $sessionId")
- onFailure()
- }
+ matrixSessionCache.getOrRestore(sessionId).onSuccess {
+ Timber.v("Succeed to restore session $sessionId")
+ onSuccess(sessionId)
+ }.onFailure {
+ Timber.e(it, "Failed to restore session $sessionId")
+ onFailure()
+ }
}
private suspend fun tryToRestoreLatestSession(
- onSuccess: (SessionId) -> Unit,
- onFailure: () -> Unit
+ onSuccess: (SessionId) -> Unit, onFailure: () -> Unit
) {
- val latestSessionId = authenticationService.getLatestSessionId()
+ val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) {
onFailure()
return
@@ -170,56 +177,70 @@ class RootFlowNode @AssistedInject constructor(
modifier = modifier,
onOpenBugReport = this::onOpenBugReport,
) {
- BackstackView()
+ val backstackSlider = rememberBackstackSlider(
+ transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
+ )
+ val backstackFader = rememberBackstackFader(
+ transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
+ )
+ val transitionHandler = rememberDelegateTransitionHandler { navTarget ->
+ when (navTarget) {
+ is NavTarget.SplashScreen,
+ is NavTarget.LoggedInFlow -> backstackFader
+ else -> backstackSlider
+ }
+ }
+ BackstackView(transitionHandler = transitionHandler)
+ announcementService.Render(Modifier)
}
}
sealed interface NavTarget : Parcelable {
- @Parcelize
- data object SplashScreen : NavTarget
+ @Parcelize data object SplashScreen : NavTarget
+
+ @Parcelize data class AccountSelect(
+ val currentSessionId: SessionId,
+ val intent: Intent?,
+ val permalinkData: PermalinkData?,
+ ) : NavTarget
- @Parcelize
- data class NotLoggedInFlow(
+ @Parcelize data class NotLoggedInFlow(
val params: LoginParams?
) : NavTarget
- @Parcelize
- data class LoggedInFlow(
- val sessionId: SessionId,
- val navId: Int
+ @Parcelize data class LoggedInFlow(
+ val sessionId: SessionId, val navId: Int
) : NavTarget
- @Parcelize
- data class SignedOutFlow(
+ @Parcelize data class SignedOutFlow(
val sessionId: SessionId
) : NavTarget
- @Parcelize
- data object BugReport : NavTarget
-
- @Parcelize
- data class ViewLogs(
- val rootPath: String,
- ) : NavTarget
+ @Parcelize data object BugReport : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.LoggedInFlow -> {
- val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
- Timber.w("Couldn't find any session, go through SplashScreen")
- }
+ val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId)
+ ?: return emptyNode(buildContext).also {
+ Timber.w("Couldn't find any session, go through SplashScreen")
+ }
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
val callback = object : LoggedInAppScopeFlowNode.Callback {
- override fun onOpenBugReport() {
+ override fun navigateToBugReport() {
backstack.push(NavTarget.BugReport)
}
+
+ override fun navigateToAddAccount() {
+ backstack.push(NavTarget.NotLoggedInFlow(null))
+ }
}
createNode(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.NotLoggedInFlow -> {
val callback = object : NotLoggedInFlowNode.Callback {
- override fun onOpenBugReport() {
+ override fun navigateToBugReport() {
backstack.push(NavTarget.BugReport)
}
}
@@ -229,54 +250,59 @@ class RootFlowNode @AssistedInject constructor(
createNode(buildContext, plugins = listOf(params, callback))
}
is NavTarget.SignedOutFlow -> {
- signedOutEntryPoint.nodeBuilder(this, buildContext)
- .params(
- SignedOutEntryPoint.Params(
- sessionId = navTarget.sessionId
- )
- )
- .build()
+ signedOutEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = SignedOutEntryPoint.Params(
+ sessionId = navTarget.sessionId,
+ ),
+ )
}
- NavTarget.SplashScreen -> splashNode(buildContext)
+ NavTarget.SplashScreen -> emptyNode(buildContext)
NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback {
- override fun onBugReportSent() {
+ override fun onDone() {
backstack.pop()
}
-
- override fun onViewLogs(basePath: String) {
- backstack.push(NavTarget.ViewLogs(rootPath = basePath))
- }
}
- bugReportEntryPoint
- .nodeBuilder(this, buildContext)
- .callback(callback)
- .build()
+ bugReportEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ callback = callback,
+ )
}
- is NavTarget.ViewLogs -> {
- val callback = object : ViewFolderEntryPoint.Callback {
- override fun onDone() {
+ is NavTarget.AccountSelect -> {
+ val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
+ override fun onAccountSelected(sessionId: SessionId) {
+ lifecycleScope.launch {
+ if (sessionId == navTarget.currentSessionId) {
+ // Ensure that the account selection Node is removed from the backstack
+ // Do not pop when the account is changed to avoid a UI flicker.
+ backstack.pop()
+ }
+ attachSession(sessionId).apply {
+ if (navTarget.intent != null) {
+ attachIncomingShare(navTarget.intent)
+ } else if (navTarget.permalinkData != null) {
+ attachPermalinkData(navTarget.permalinkData)
+ }
+ }
+ }
+ }
+
+ override fun onCancel() {
backstack.pop()
}
}
- val params = ViewFolderEntryPoint.Params(
- rootPath = navTarget.rootPath,
+ accountSelectEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ callback = callback,
)
- viewFolderEntryPoint
- .nodeBuilder(this, buildContext)
- .params(params)
- .callback(callback)
- .build()
}
}
}
- private fun splashNode(buildContext: BuildContext) = node(buildContext) {
- Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- }
-
suspend fun handleIntent(intent: Intent) {
val resolvedIntent = intentResolver.resolve(intent) ?: return
when (resolvedIntent) {
@@ -289,78 +315,147 @@ class RootFlowNode @AssistedInject constructor(
}
private suspend fun onLoginLink(params: LoginParams) {
- // Is there a session already?
- val latestSessionId = authenticationService.getLatestSessionId()
- if (latestSessionId == null) {
- // No session, open login
- if (enterpriseService.isAllowedToConnectToHomeserver(params.accountProvider.ensureProtocol())) {
- switchToNotLoggedInFlow(params)
+ if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
+ // Is there a session already?
+ val sessions = sessionStore.getAllSessions()
+ if (sessions.isNotEmpty()) {
+ if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
+ val loginHintMatrixId = params.loginHint?.removePrefix("mxid:")
+ val existingAccount = sessions.find { it.userId == loginHintMatrixId }
+ if (existingAccount != null) {
+ // We have an existing account matching the login hint, ensure this is the current session
+ sessionStore.setLatestSession(existingAccount.userId)
+ } else {
+ val latestSessionId = sessions.maxBy { it.lastUsageIndex }.userId
+ attachSession(SessionId(latestSessionId))
+ backstack.push(NavTarget.NotLoggedInFlow(params))
+ }
+ } else {
+ Timber.w("Login link ignored, multi account is disabled")
+ }
} else {
- Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
- switchToNotLoggedInFlow(null)
+ switchToNotLoggedInFlow(params)
}
} else {
- // Just ignore the login link if we already have a session
- Timber.w("Login link ignored, we already have a session")
+ Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
}
}
private suspend fun onIncomingShare(intent: Intent) {
// Is there a session already?
- val latestSessionId = authenticationService.getLatestSessionId()
+ val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) {
// No session, open login
switchToNotLoggedInFlow(null)
} else {
- attachSession(latestSessionId)
- .attachIncomingShare(intent)
+ // wait for the current session to be restored
+ val loggedInFlowNode = attachSession(latestSessionId)
+ if (sessionStore.numberOfSessions() > 1) {
+ // Several accounts, let the user choose which one to use
+ backstack.push(
+ NavTarget.AccountSelect(
+ currentSessionId = latestSessionId,
+ intent = intent,
+ permalinkData = null,
+ )
+ )
+ } else {
+ // Only one account, directly attach the incoming share node.
+ loggedInFlowNode.attachIncomingShare(intent)
+ }
}
}
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,
- trigger = JoinedRoom.Trigger.MobilePermalink,
- serverNames = permalinkData.viaParameters,
- eventId = permalinkData.eventId,
- clearBackstack = true
+ // Is there a session already?
+ val latestSessionId = sessionStore.getLatestSessionId()
+ if (latestSessionId == null) {
+ // No session, open login
+ switchToNotLoggedInFlow(null)
+ } else {
+ // wait for the current session to be restored
+ val loggedInFlowNode = attachSession(latestSessionId)
+ when (permalinkData) {
+ is PermalinkData.FallbackLink -> Unit
+ is PermalinkData.RoomEmailInviteLink -> Unit
+ else -> {
+ if (sessionStore.numberOfSessions() > 1) {
+ // Several accounts, let the user choose which one to use
+ backstack.push(
+ NavTarget.AccountSelect(
+ currentSessionId = latestSessionId,
+ intent = null,
+ permalinkData = permalinkData,
+ )
)
+ } else {
+ // Only one account, directly attach the room or the user node.
+ loggedInFlowNode.attachPermalinkData(permalinkData)
}
- is PermalinkData.UserLink -> {
- attachUser(permalinkData.userId)
- }
}
}
+ }
+ }
+
+ private suspend fun LoggedInFlowNode.attachPermalinkData(permalinkData: PermalinkData) {
+ when (permalinkData) {
+ is PermalinkData.FallbackLink -> Unit
+ is PermalinkData.RoomEmailInviteLink -> Unit
+ is PermalinkData.RoomLink -> {
+ // If there is a thread id, focus on it in the main timeline
+ val focusedEventId = if (permalinkData.threadId != null) {
+ permalinkData.threadId?.asEventId()
+ } else {
+ permalinkData.eventId
+ }
+ attachRoom(
+ roomIdOrAlias = permalinkData.roomIdOrAlias,
+ trigger = JoinedRoom.Trigger.MobilePermalink,
+ serverNames = permalinkData.viaParameters,
+ eventId = focusedEventId,
+ clearBackstack = true
+ ).maybeAttachThread(permalinkData.threadId, permalinkData.eventId)
+ }
+ is PermalinkData.UserLink -> {
+ attachUser(permalinkData.userId)
+ }
+ }
+ }
+
+ private suspend fun RoomFlowNode.maybeAttachThread(threadId: ThreadId?, focusedEventId: EventId?) {
+ if (threadId != null) {
+ attachThread(threadId, focusedEventId)
+ }
}
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
Timber.d("Navigating to $deeplinkData")
- attachSession(deeplinkData.sessionId)
- .apply {
- when (deeplinkData) {
- is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
- is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
+ attachSession(deeplinkData.sessionId).let { loggedInFlowNode ->
+ when (deeplinkData) {
+ is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
+ is DeeplinkData.Room -> {
+ loggedInFlowNode.attachRoom(
+ roomIdOrAlias = deeplinkData.roomId.toRoomIdOrAlias(),
+ eventId = if (deeplinkData.threadId != null) deeplinkData.threadId?.asEventId() else deeplinkData.eventId,
+ clearBackstack = true,
+ ).maybeAttachThread(deeplinkData.threadId, deeplinkData.eventId)
}
}
+ }
}
private fun onOidcAction(oidcAction: OidcAction) {
oidcActionFlow.post(oidcAction)
}
- // [sessionId] will be null for permalink.
- private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
- // TODO handle multi-session
+ private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
+ // Ensure that the session is the latest one
+ sessionStore.setLatestSession(sessionId.value)
return waitForChildAttached { navTarget ->
- navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
- }
- .attachSession()
+ navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
+ }.attachSession()
}
}
+
+private suspend fun SessionStore.getLatestSessionId() = getLatestSession()?.userId?.let(::SessionId)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt
index 302a12d99b9..4af64cba34c 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,9 +11,9 @@ package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -22,7 +23,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
@@ -33,7 +33,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold
*/
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class MatrixSessionCache @Inject constructor(
+class MatrixSessionCache(
private val authenticationService: MatrixAuthenticationService,
private val syncOrchestratorFactory: SyncOrchestrator.Factory,
) : MatrixClientProvider {
@@ -42,12 +42,7 @@ class MatrixSessionCache @Inject constructor(
init {
authenticationService.listenToNewMatrixClients { matrixClient ->
- val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
- sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
- matrixClient = matrixClient,
- syncOrchestrator = syncOrchestrator,
- )
- syncOrchestrator.start()
+ onNewMatrixClient(matrixClient)
}
}
@@ -105,17 +100,24 @@ class MatrixSessionCache @Inject constructor(
Timber.d("Restore matrix session: $sessionId")
return authenticationService.restoreSession(sessionId)
.onSuccess { matrixClient ->
- val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
- sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
- matrixClient = matrixClient,
- syncOrchestrator = syncOrchestrator,
- )
- syncOrchestrator.start()
+ onNewMatrixClient(matrixClient)
}
.onFailure {
Timber.e(it, "Fail to restore session")
}
}
+
+ private fun onNewMatrixClient(matrixClient: MatrixClient) {
+ val syncOrchestrator = syncOrchestratorFactory.create(
+ syncService = matrixClient.syncService,
+ sessionCoroutineScope = matrixClient.sessionCoroutineScope,
+ )
+ sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
+ matrixClient = matrixClient,
+ syncOrchestrator = syncOrchestrator,
+ )
+ syncOrchestrator.start()
+ }
}
private data class InMemoryMatrixSession(
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt
deleted file mode 100644
index 4911ab50b50..00000000000
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.appnav.di
-
-import io.element.android.libraries.matrix.api.room.JoinedRoom
-
-interface RoomComponentFactory {
- fun create(room: JoinedRoom): Any
-}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt
new file mode 100644
index 00000000000..2d5244cb5a3
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomGraphFactory.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.appnav.di
+
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+
+fun interface RoomGraphFactory {
+ fun create(room: JoinedRoom): Any
+}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt
deleted file mode 100644
index 8800043b280..00000000000
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/SessionComponentFactory.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.appnav.di
-
-import io.element.android.libraries.matrix.api.MatrixClient
-
-interface SessionComponentFactory {
- fun create(client: MatrixClient): Any
-}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt
new file mode 100644
index 00000000000..bc3960409e6
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SessionGraphFactory.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.appnav.di
+
+import io.element.android.libraries.matrix.api.MatrixClient
+
+interface SessionGraphFactory {
+ fun create(client: MatrixClient): Any
+}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
index d3ab19466f6..53ce50a7889 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
@@ -1,23 +1,27 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
-import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
+import io.element.android.services.analytics.api.AnalyticsService
+import io.element.android.services.analytics.api.recordTransaction
import io.element.android.services.appnavstate.api.AppForegroundStateService
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
@@ -30,22 +34,26 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
-class SyncOrchestrator @AssistedInject constructor(
- @Assisted matrixClient: MatrixClient,
+@AssistedInject
+class SyncOrchestrator(
+ @Assisted private val syncService: SyncService,
+ @Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
dispatchers: CoroutineDispatchers,
+ private val analyticsService: AnalyticsService,
) {
@AssistedFactory
interface Factory {
- fun create(matrixClient: MatrixClient): SyncOrchestrator
+ fun create(
+ syncService: SyncService,
+ sessionCoroutineScope: CoroutineScope,
+ ): SyncOrchestrator
}
- private val syncService = matrixClient.syncService()
-
private val tag = "SyncOrchestrator"
- private val coroutineScope = matrixClient.sessionCoroutineScope.childScope(dispatchers.io, tag)
+ private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
@@ -64,10 +72,13 @@ class SyncOrchestrator @AssistedInject constructor(
// Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible
// Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline
Timber.tag(tag).d("performing initial sync attempt")
- syncService.startSync()
+ analyticsService.recordTransaction("First sync", "syncService.startSync()") { transaction ->
+ syncService.startSync()
- // Wait until the sync service is not idle, either it will be running or in error/offline state
- syncService.syncState.first { it != SyncState.Idle }
+ // Wait until the sync service is not idle, either it will be running or in error/offline state
+ val firstState = syncService.syncState.first { it != SyncState.Idle }
+ transaction.setData("first_sync_state", firstState.name)
+ }
observeStates()
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt
new file mode 100644
index 00000000000..cb78760a0f2
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.appnav.di
+
+import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
+import io.element.android.libraries.matrix.api.timeline.TimelineProvider
+
+interface TimelineBindings {
+ val timelineProvider: TimelineProvider
+ val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
+}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
index 786b694c3fd..3e26130c78a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -1,23 +1,24 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.intent
import android.content.Intent
+import dev.zacsweers.metro.Inject
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
-import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.deeplink.DeeplinkParser
+import io.element.android.libraries.deeplink.api.DeeplinkData
+import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcIntentResolver
import timber.log.Timber
-import javax.inject.Inject
sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
@@ -27,7 +28,8 @@ sealed interface ResolvedIntent {
data class IncomingShare(val intent: Intent) : ResolvedIntent
}
-class IntentResolver @Inject constructor(
+@Inject
+class IntentResolver(
private val deeplinkParser: DeeplinkParser,
private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver,
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateExt.kt
index 565dbdd5adf..714645dcdc3 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateExt.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateExt.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt
index d7020c50a56..d2a9c9298e6 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt
index b337d32cdf9..c49ca4221de 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,14 +13,15 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class LoggedInNode @AssistedInject constructor(
+@AssistedInject
+class LoggedInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val loggedInPresenter: LoggedInPresenter,
@@ -31,18 +33,14 @@ class LoggedInNode @AssistedInject constructor(
fun navigateToNotificationTroubleshoot()
}
- private fun navigateToNotificationTroubleshoot() {
- plugins().forEach {
- it.navigateToNotificationTroubleshoot()
- }
- }
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val loggedInState = loggedInPresenter.present()
LoggedInView(
state = loggedInState,
- navigateToNotificationTroubleshoot = ::navigateToNotificationTroubleshoot,
+ navigateToNotificationTroubleshoot = callback::navigateToNotificationTroubleshoot,
modifier = modifier
)
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
index 7ef73986ac8..184766e3230 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -17,6 +18,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.libraries.architecture.AsyncData
@@ -34,7 +36,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.push.api.PushService
-import io.element.android.libraries.pushproviders.api.RegistrationFailure
+import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
@@ -42,11 +44,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
-import javax.inject.Inject
private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag)
-class LoggedInPresenter @Inject constructor(
+@Inject
+class LoggedInPresenter(
private val matrixClient: MatrixClient,
private val syncService: SyncService,
private val pushService: PushService,
@@ -69,7 +71,17 @@ class LoggedInPresenter @Inject constructor(
when (sessionVerifiedStatus) {
SessionVerifiedStatus.Unknown -> Unit
SessionVerifiedStatus.Verified -> {
- ensurePusherIsRegistered(pusherRegistrationState)
+ Timber.tag(pusherTag.value).d("Ensure pusher is registered")
+ pushService.ensurePusherIsRegistered(matrixClient).fold(
+ onSuccess = {
+ Timber.tag(pusherTag.value).d("Pusher registered")
+ pusherRegistrationState.value = AsyncData.Success(Unit)
+ },
+ onFailure = {
+ Timber.tag(pusherTag.value).e(it, "Failed to register pusher")
+ pusherRegistrationState.value = AsyncData.Failure(it)
+ },
+ )
}
SessionVerifiedStatus.NotVerified -> {
pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.AccountNotVerified())
@@ -121,7 +133,7 @@ class LoggedInPresenter @Inject constructor(
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = buildMeta.applicationName,
- eventSink = ::handleEvent
+ eventSink = ::handleEvent,
)
}
@@ -131,59 +143,6 @@ class LoggedInPresenter @Inject constructor(
currentSlidingSyncVersion == SlidingSyncVersion.Proxy
}
- private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState>) {
- Timber.tag(pusherTag.value).d("Ensure pusher is registered")
- val currentPushProvider = pushService.getCurrentPushProvider()
- val result = if (currentPushProvider == null) {
- Timber.tag(pusherTag.value).d("Register with the first available push provider with at least one distributor")
- val pushProvider = pushService.getAvailablePushProviders()
- .firstOrNull { it.getDistributors().isNotEmpty() }
- // Else fallback to the first available push provider (the list should never be empty)
- ?: pushService.getAvailablePushProviders().firstOrNull()
- ?: return Unit
- .also { Timber.tag(pusherTag.value).w("No push providers available") }
- .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable()) }
- val distributor = pushProvider.getDistributors().firstOrNull()
- ?: return Unit
- .also { Timber.tag(pusherTag.value).w("No distributors available") }
- .also {
- // In this case, consider the push provider is chosen.
- pushService.selectPushProvider(matrixClient.sessionId, pushProvider)
- }
- .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
- pushService.registerWith(matrixClient, pushProvider, distributor)
- } else {
- val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient.sessionId)
- if (currentPushDistributor == null) {
- Timber.tag(pusherTag.value).d("Register with the first available distributor")
- val distributor = currentPushProvider.getDistributors().firstOrNull()
- ?: return Unit
- .also { Timber.tag(pusherTag.value).w("No distributors available") }
- .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
- pushService.registerWith(matrixClient, currentPushProvider, distributor)
- } else {
- Timber.tag(pusherTag.value).d("Re-register with the current distributor")
- pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
- }
- }
- result.fold(
- onSuccess = {
- Timber.tag(pusherTag.value).d("Pusher registered")
- pusherRegistrationState.value = AsyncData.Success(Unit)
- },
- onFailure = {
- Timber.tag(pusherTag.value).e(it, "Failed to register pusher")
- if (it is RegistrationFailure) {
- pusherRegistrationState.value = AsyncData.Failure(
- PusherRegistrationFailure.RegistrationFailure(it.clientException, it.isRegisteringAgain)
- )
- } else {
- pusherRegistrationState.value = AsyncData.Failure(it)
- }
- }
- )
- }
-
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()
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
index 8f039ffa2be..b066f9f867b 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
index 2ba0c8de81d..b2f54075198 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,6 +10,7 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStateProvider : PreviewParameterProvider {
override val values: Sequence
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
index d6a12ea27bd..62d8de8c297 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -24,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.exception.isNetworkError
+import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt
index d9ed15318a9..6b5803aa148 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt
@@ -1,12 +1,14 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.loggedin
+import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -14,13 +16,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
-import javax.inject.Inject
/**
* This migration is temporary, will be safe to remove after some time.
* The goal is to set the server config if it's not set, and remove the local data.
*/
-class MediaPreviewConfigMigration @Inject constructor(
+@Inject
+class MediaPreviewConfigMigration(
private val mediaPreviewService: MediaPreviewService,
private val appPreferencesStore: AppPreferencesStore,
@SessionCoroutineScope
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt
index cbb247569a5..3f01f8337ef 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt
@@ -1,16 +1,18 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.loggedin
import androidx.annotation.VisibleForTesting
+import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
@@ -21,13 +23,13 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
-import javax.inject.Inject
@VisibleForTesting
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L
@SingleIn(SessionScope::class)
-class SendQueues @Inject constructor(
+@Inject
+class SendQueues(
private val matrixClient: MatrixClient,
private val syncService: SyncService,
) {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
index e8890a5da8b..7c5f196a4c8 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
index 103e97b2583..22e1d925c9f 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,8 +10,6 @@ package io.element.android.appnav.room
import android.os.Parcelable
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext
@@ -21,37 +20,41 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.joined.LoadingRoomNodeView
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
+import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.core.coroutine.withPreviousValue
import io.element.android.libraries.di.SessionScope
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.RoomAlias
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.ThreadId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
-import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@@ -59,13 +62,13 @@ import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@ContributesNode(SessionScope::class)
-class RoomFlowNode @AssistedInject constructor(
+@AssistedInject
+class RoomFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
private val client: MatrixClient,
private val joinRoomEntryPoint: JoinRoomEntryPoint,
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
- private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver,
) : BaseFlowNode(
backstack = BackStack(
@@ -123,30 +126,31 @@ class RoomFlowNode @AssistedInject constructor(
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) {
val roomInfoFlow = client.getRoomInfoFlow(roomId)
- val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
- val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged()
- combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
+
+ // This observes the local membership changes for the room
+ val membershipUpdateFlow = membershipObserver.updates
+ .filter { it.roomId == roomId }
+ .distinctUntilChanged()
+ // We add a replay so we can check the last local membership update
+ .shareIn(lifecycleScope, started = SharingStarted.Eagerly, replay = 1)
+
+ val currentMembershipFlow = roomInfoFlow
+ .map { it.getOrNull()?.currentUserMembership }
+ .distinctUntilChanged()
+ .withPreviousValue()
+ currentMembershipFlow.onEach { (previousMembership, membership) ->
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 = roomId,
- serverNames = serverNames,
- trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
- )
- )
- } else {
- backstack.newRoot(NavTarget.JoinedRoom(roomId))
- }
- }
- else -> {
- // Was invited or the room is not known, display the join room screen
+ if (membership == CurrentUserMembership.JOINED) {
+ backstack.newRoot(NavTarget.JoinedRoom(roomId))
+ } else {
+ val leavingFromCurrentDevice =
+ membership == CurrentUserMembership.LEFT &&
+ previousMembership == CurrentUserMembership.JOINED &&
+ membershipUpdateFlow.replayCache.lastOrNull()?.isUserInRoom == false
+
+ if (leavingFromCurrentDevice) {
+ navigateUp()
+ } else {
backstack.newRoot(
NavTarget.JoinRoom(
roomId = roomId,
@@ -157,15 +161,6 @@ class RoomFlowNode @AssistedInject constructor(
}
}
}.launchIn(lifecycleScope)
-
- // If the user leaves the room from this client, close the room flow.
- lifecycleScope.launch {
- membershipObserver.updates
- .first { it.roomId == roomId && !it.isUserInRoom }
- .run {
- navigateUp()
- }
- }
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -180,11 +175,13 @@ class RoomFlowNode @AssistedInject constructor(
)
}
}
- val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
- roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
- .callback(callback)
- .params(params)
- .build()
+ val params = Params(navTarget.roomAlias)
+ roomAliasResolverEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = params,
+ callback = callback,
+ )
}
is NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(
@@ -194,7 +191,11 @@ class RoomFlowNode @AssistedInject constructor(
serverNames = navTarget.serverNames,
trigger = navTarget.trigger,
)
- joinRoomEntryPoint.createNode(this, buildContext, inputs)
+ joinRoomEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ inputs = inputs,
+ )
}
is NavTarget.JoinedRoom -> {
val roomFlowNodeCallback = plugins()
@@ -207,11 +208,14 @@ class RoomFlowNode @AssistedInject constructor(
}
}
+ suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
+ waitForChildAttached()
+ .attachThread(threadId, focusedEventId)
+ }
+
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
- val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView(
state = LoadingRoomState.Loading,
- hasNetworkConnection = isOnline,
onBackClick = { navigateUp() },
modifier = modifier,
)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt
index ab1589898b6..93403371c90 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +14,9 @@ import kotlinx.parcelize.Parcelize
sealed interface RoomNavigationTarget : Parcelable {
@Parcelize
- data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget
+ data class Root(
+ val eventId: EventId? = null,
+ ) : RoomNavigationTarget
@Parcelize
data object Details : RoomNavigationTarget
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt
index 07580060c73..85710deffea 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -24,9 +25,9 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
@@ -34,8 +35,9 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
+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.sync.SyncService
+import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -45,11 +47,11 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class JoinedRoomFlowNode @AssistedInject constructor(
+@AssistedInject
+class JoinedRoomFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
- private val syncService: SyncService,
) :
BaseFlowNode(
backstack = BackStack(
@@ -115,15 +117,18 @@ class JoinedRoomFlowNode @AssistedInject constructor(
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
- val isOnline by syncService.isOnline.collectAsState()
LoadingRoomNodeView(
state = loadingRoomState,
- hasNetworkConnection = isOnline,
- modifier = modifier,
- onBackClick = onBackClick
+ onBackClick = onBackClick,
+ modifier = modifier
)
}
+ suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
+ waitForChildAttached()
+ .attachThread(threadId, focusedEventId)
+ }
+
@Composable
override fun View(modifier: Modifier) {
BackstackView(
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
index 3e5947aada9..dab636031f0 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,24 +17,31 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.appnav.di.RoomComponentFactory
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.appnav.di.RoomGraphFactory
+import io.element.android.appnav.di.TimelineBindings
import io.element.android.appnav.room.RoomNavigationTarget
+import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
+import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.di.DaggerComponentOwner
+import io.element.android.libraries.architecture.waitForChildAttached
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
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.ThreadId
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.JoinedRoom
@@ -45,34 +53,32 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(SessionScope::class)
-class JoinedRoomLoadedFlowNode @AssistedInject constructor(
+@AssistedInject
+class JoinedRoomLoadedFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val messagesEntryPoint: MessagesEntryPoint,
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
+ private val spaceEntryPoint: SpaceEntryPoint,
+ private val forwardEntryPoint: ForwardEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder,
- roomComponentFactory: RoomComponentFactory,
+ roomGraphFactory: RoomGraphFactory,
) : BaseFlowNode(
backstack = BackStack(
- initialElement = when (val input = plugins.filterIsInstance().first().initialElement) {
- is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
- RoomNavigationTarget.Details -> NavTarget.RoomDetails
- RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
- },
+ initialElement = initialElement(plugins),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
-), DaggerComponentOwner {
+), DependencyInjectionGraphOwner {
interface Callback : Plugin {
- fun onOpenRoom(roomId: RoomId)
- fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
- fun onForwardedToSingleRoom(roomId: RoomId)
- fun onOpenGlobalNotificationSettings()
+ fun navigateToRoom(roomId: RoomId, serverNames: List)
+ fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
+ fun navigateToGlobalNotificationSettings()
}
data class Inputs(
@@ -81,8 +87,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
) : NodeInputs
private val inputs: Inputs = inputs()
- private val callbacks = plugins.filterIsInstance()
- override val daggerComponent = roomComponentFactory.create(inputs.room)
+ private val callback: Callback = callback()
+ override val graph = roomGraphFactory.create(inputs.room)
init {
lifecycle.subscribe(
@@ -117,26 +123,28 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
val callback = object : RoomDetailsEntryPoint.Callback {
- override fun onOpenGlobalNotificationSettings() {
- callbacks.forEach { it.onOpenGlobalNotificationSettings() }
+ override fun navigateToGlobalNotificationSettings() {
+ callback.navigateToGlobalNotificationSettings()
}
- override fun onOpenRoom(roomId: RoomId) {
- callbacks.forEach { it.onOpenRoom(roomId) }
+ override fun navigateToRoom(roomId: RoomId, serverNames: List) {
+ callback.navigateToRoom(roomId, serverNames)
}
- override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
- callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) }
+ override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
+ callback.handlePermalinkClick(data, pushToBackstack)
}
- override fun onForwardedToSingleRoom(roomId: RoomId) {
- callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
+ override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) {
+ backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
}
}
- return roomDetailsEntryPoint.nodeBuilder(this, buildContext)
- .params(RoomDetailsEntryPoint.Params(initialTarget))
- .callback(callback)
- .build()
+ return roomDetailsEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = RoomDetailsEntryPoint.Params(initialTarget),
+ callback = callback,
+ )
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -153,55 +161,140 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
NavTarget.RoomNotificationSettings -> {
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings)
}
+ NavTarget.RoomMemberList -> {
+ createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomMemberList)
+ }
+ NavTarget.Space -> {
+ createSpaceNode(buildContext)
+ }
+ is NavTarget.ForwardEvent -> {
+ val timelineProvider = if (navTarget.fromPinnedEvents) {
+ (graph as TimelineBindings).pinnedEventsTimelineProvider
+ } else {
+ (graph as TimelineBindings).timelineProvider
+ }
+ val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
+ val callback = object : ForwardEntryPoint.Callback {
+ override fun onDone(roomIds: List) {
+ backstack.pop()
+ roomIds.singleOrNull()?.let { roomId ->
+ callback.navigateToRoom(roomId, emptyList())
+ }
+ }
+ }
+ forwardEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = params,
+ callback = callback,
+ )
+ }
}
}
+ private fun createSpaceNode(buildContext: BuildContext): Node {
+ val callback = object : SpaceEntryPoint.Callback {
+ override fun navigateToRoom(roomId: RoomId, viaParameters: List) {
+ callback.navigateToRoom(roomId, viaParameters)
+ }
+
+ override fun navigateToRoomMemberList() {
+ backstack.push(NavTarget.RoomMemberList)
+ }
+ }
+ return spaceEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ inputs = SpaceEntryPoint.Inputs(roomId = inputs.room.roomId),
+ callback = callback,
+ )
+ }
+
private fun createMessagesNode(
buildContext: BuildContext,
navTarget: NavTarget.Messages,
): Node {
val callback = object : MessagesEntryPoint.Callback {
- override fun onRoomDetailsClick() {
+ override fun navigateToRoomDetails() {
backstack.push(NavTarget.RoomDetails)
}
- override fun onUserDataClick(userId: UserId) {
+ override fun navigateToRoomMemberDetails(userId: UserId) {
backstack.push(NavTarget.RoomMemberDetails(userId))
}
- override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
- callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) }
+ override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
+ callback.handlePermalinkClick(data, pushToBackstack)
+ }
+
+ override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
+ backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
}
- override fun onForwardedToSingleRoom(roomId: RoomId) {
- callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
+ override fun navigateToRoom(roomId: RoomId) {
+ callback.navigateToRoom(roomId, emptyList())
}
}
val params = MessagesEntryPoint.Params(
MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId)
)
- return messagesEntryPoint.nodeBuilder(this, buildContext)
- .params(params)
- .callback(callback)
- .build()
+ return messagesEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = params,
+ callback = callback,
+ )
}
sealed interface NavTarget : Parcelable {
@Parcelize
- data class Messages(val focusedEventId: EventId? = null) : NavTarget
+ data object Space : NavTarget
+
+ @Parcelize
+ data class Messages(
+ val focusedEventId: EventId? = null,
+ ) : NavTarget
@Parcelize
data object RoomDetails : NavTarget
+ @Parcelize
+ data object RoomMemberList : NavTarget
+
@Parcelize
data class RoomMemberDetails(val userId: UserId) : NavTarget
+ @Parcelize
+ data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget
+
@Parcelize
data object RoomNotificationSettings : NavTarget
}
+ suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) {
+ val messageNode = waitForChildAttached { navTarget ->
+ navTarget is NavTarget.Messages
+ }
+ (messageNode as? MessagesEntryPoint.NodeProxy)?.attachThread(threadId, focusedEventId)
+ }
+
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
+
+private fun initialElement(plugins: List): JoinedRoomLoadedFlowNode.NavTarget {
+ val input = plugins.filterIsInstance().single()
+ return when (input.initialElement) {
+ is RoomNavigationTarget.Root -> {
+ if (input.room.roomInfoFlow.value.isSpace) {
+ JoinedRoomLoadedFlowNode.NavTarget.Space
+ } else {
+ JoinedRoomLoadedFlowNode.NavTarget.Messages(input.initialElement.eventId)
+ }
+ }
+ RoomNavigationTarget.Details -> JoinedRoomLoadedFlowNode.NavTarget.RoomDetails
+ RoomNavigationTarget.NotificationSettings -> JoinedRoomLoadedFlowNode.NavTarget.RoomNotificationSettings
+ }
+}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
index 938e46f9156..a596052fca8 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
@@ -1,15 +1,14 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.room.joined
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -21,9 +20,6 @@ import androidx.compose.ui.res.stringResource
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.networkmonitor.api.ui.ConnectivityIndicatorView
-import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
-import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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
@@ -31,6 +27,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
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.designsystem.utils.DelayedVisibility
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
import io.element.android.libraries.ui.strings.CommonStrings
@@ -38,17 +35,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LoadingRoomNodeView(
state: LoadingRoomState,
- hasNetworkConnection: Boolean,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = {
- Column {
- ConnectivityIndicatorView(isOnline = hasNetworkConnection)
- LoadingRoomTopBar(onBackClick)
- }
+ LoadingRoomTopBar(onBackClick)
},
content = { padding ->
Box(
@@ -66,7 +59,9 @@ fun LoadingRoomNodeView(
style = ElementTheme.typography.fontBodyMdRegular,
)
} else {
- CircularProgressIndicator()
+ DelayedVisibility {
+ CircularProgressIndicator()
+ }
}
}
},
@@ -83,9 +78,7 @@ private fun LoadingRoomTopBar(
BackButton(onClick = onBackClick)
},
title = {
- IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
},
- windowInsets = WindowInsets(0.dp),
)
}
@@ -94,7 +87,6 @@ private fun LoadingRoomTopBar(
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView(
state = state,
- onBackClick = {},
- hasNetworkConnection = false
+ onBackClick = {}
)
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt
index a98ad0e0fd0..9015872e62c 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
index 08e999245bf..9a91e9765bf 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,18 +10,16 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
+import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.MatrixSessionCache
-import io.element.android.features.login.api.LoginUserStory
import io.element.android.features.preferences.api.CacheService
-import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
-import io.element.android.libraries.sessionstorage.api.LoggedInState
+import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
-import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY"
@@ -28,12 +27,12 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact
* This class is responsible for creating a flow of [RootNavState].
* It gathers data from multiple datasource and creates a unique one.
*/
-class RootNavStateFlowFactory @Inject constructor(
- private val authenticationService: MatrixAuthenticationService,
+@Inject
+class RootNavStateFlowFactory(
+ private val sessionStore: SessionStore,
private val cacheService: CacheService,
private val matrixSessionCache: MatrixSessionCache,
private val imageLoaderHolder: ImageLoaderHolder,
- private val loginUserStory: LoginUserStory,
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
) {
private var currentCacheIndex = 0
@@ -41,14 +40,12 @@ class RootNavStateFlowFactory @Inject constructor(
fun create(savedStateMap: SavedStateMap?): Flow {
return combine(
cacheIndexFlow(savedStateMap),
- authenticationService.loggedInStateFlow(),
- loginUserStory.loginFlowIsDone,
- ) { cacheIndex, loggedInState, loginFlowIsDone ->
- if (loginFlowIsDone) {
- RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState)
- } else {
- RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn)
- }
+ sessionStore.loggedInStateFlow(),
+ ) { cacheIndex, loggedInState ->
+ RootNavState(
+ cacheIndex = cacheIndex,
+ loggedInState = loggedInState,
+ )
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt
index 229efd7d74a..75704c499d6 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,22 +12,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.SuperProperties
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
-import io.element.android.features.share.api.ShareService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.apperror.api.AppErrorStateService
-import javax.inject.Inject
-class RootPresenter @Inject constructor(
+@Inject
+class RootPresenter(
private val crashDetectionPresenter: Presenter,
private val rageshakeDetectionPresenter: Presenter,
private val appErrorStateService: AppErrorStateService,
private val analyticsService: AnalyticsService,
- private val shareService: ShareService,
private val sdkMetadata: SdkMetadata,
) : Presenter {
@Composable
@@ -45,10 +45,6 @@ class RootPresenter @Inject constructor(
)
}
- LaunchedEffect(Unit) {
- shareService.observeFeatureFlag(this)
- }
-
return RootState(
rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState,
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt
index 3ea7362efaa..0d7f362c1e4 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt
@@ -1,18 +1,17 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.root
-import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState
-@Immutable
data class RootState(
val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState,
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt
index 4d84e06070a..26db2054987 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
index bd7db5e9c23..2bc76d7a5bb 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/main/res/values-de/translations.xml b/appnav/src/main/res/values-de/translations.xml
index 339d346a6b7..13d085e3dc2 100644
--- a/appnav/src/main/res/values-de/translations.xml
+++ b/appnav/src/main/res/values-de/translations.xml
@@ -1,6 +1,6 @@
"Abmelden und aktualisieren"
- "%1$s unterstützt das alte Protokoll nicht mehr. Bitte melden Sie sich ab und wieder an, um die App weiter nutzen zu können."
+ "%1$s unterstützt das alte Protokoll nicht mehr. Bitte melde dich ab und wieder an, um die App weiter nutzen zu können.""Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."
diff --git a/appnav/src/main/res/values-fa/translations.xml b/appnav/src/main/res/values-fa/translations.xml
index 3bb3bca2911..16bb7fd3e21 100644
--- a/appnav/src/main/res/values-fa/translations.xml
+++ b/appnav/src/main/res/values-fa/translations.xml
@@ -1,4 +1,5 @@
"خروج و ارتقا"
+ "%1$s دیگر از شیوهنامهٔ قدیمی پشتیبانی نمیکند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."
diff --git a/appnav/src/main/res/values-ko/translations.xml b/appnav/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..a29e74f46c8
--- /dev/null
+++ b/appnav/src/main/res/values-ko/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "로그아웃 및 업그레이드"
+ "%1$s 더 이상 이전 프로토콜을 지원하지 않습니다. 계속 사용하려면 로그아웃 후 다시 로그인해 주세요."
+ "귀하의 홈서버는 더 이상 이전 프로토콜을 지원하지 않습니다. 앱을 계속 사용하려면 로그아웃한 후 다시 로그인하세요."
+
diff --git a/appnav/src/main/res/values-pt-rBR/translations.xml b/appnav/src/main/res/values-pt-rBR/translations.xml
index 385e07d7d19..90d27d3a44f 100644
--- a/appnav/src/main/res/values-pt-rBR/translations.xml
+++ b/appnav/src/main/res/values-pt-rBR/translations.xml
@@ -1,6 +1,6 @@
"Sair e atualizar"
- "%1$s não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."
- "Seu servidor doméstico não é mais compatível com o protocolo antigo. Faça logout e login novamente para continuar usando o aplicativo."
+ "%1$s não tem mais suporte ao protocolo antigo. Saia da sua conta e entre novamente para continuar utilizando o aplicativo."
+ "Seu servidor-casa não é mais compatível com o protocolo antigo. Saia da sua conta e entre novamente para continuar usando o aplicativo."
diff --git a/appnav/src/main/res/values-pt/translations.xml b/appnav/src/main/res/values-pt/translations.xml
index d606a8d103a..2d84d29475a 100644
--- a/appnav/src/main/res/values-pt/translations.xml
+++ b/appnav/src/main/res/values-pt/translations.xml
@@ -2,5 +2,5 @@
"Sair & Atualizar""%1$s já não suporta o protocolo antigo. Termina a sessão e volta a iniciar sessão para continuares a utilizar a aplicação."
- "Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."
+ "O teu servidor já não permite o protocolo antigo. Termine sessão e volte a iniciá-la para continuar a utilizar a aplicação."
diff --git a/appnav/src/main/res/values-ro/translations.xml b/appnav/src/main/res/values-ro/translations.xml
index bed8d18506e..626902b4903 100644
--- a/appnav/src/main/res/values-ro/translations.xml
+++ b/appnav/src/main/res/values-ro/translations.xml
@@ -1,5 +1,6 @@
"Deconectați-vă și faceți upgrade"
+ "%1$s nu mai acceptă vechiul protocol. Vă rugăm să vă deconectați și să vă reconectați pentru a continua utilizarea aplicației.""Serverul dvs. de acasă nu mai acceptă vechiul protocol. Vă rugăm să vă deconectați și să vă conectați din nou pentru a continua să utilizați aplicația."
diff --git a/appnav/src/main/res/values-tr/translations.xml b/appnav/src/main/res/values-tr/translations.xml
index c0a8ccd1361..443bed5b18b 100644
--- a/appnav/src/main/res/values-tr/translations.xml
+++ b/appnav/src/main/res/values-tr/translations.xml
@@ -1,5 +1,7 @@
"Çıkış Yap ve Yükselt"
+ "%1$s artık eski protokolü destekleniyor. Uygulamayı kullanmaya devam etmek için lütfen çıkış yapın ve tekrar giriş yapın
+""Ana sunucunuz artık eski protokolü desteklemiyor. Lütfen oturumu kapatın ve uygulamayı kullanmaya devam etmek için tekrar oturum açın."
diff --git a/appnav/src/main/res/values-ur/translations.xml b/appnav/src/main/res/values-ur/translations.xml
new file mode 100644
index 00000000000..80d1840d9da
--- /dev/null
+++ b/appnav/src/main/res/values-ur/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "لاگ آؤٹ اور اپ گریڈ کریں"
+ "آپ کا homeserver اب پرانے پروٹوکول کو سپورٹ نہیں کرتا ہے۔ براہ کرم لاگ آؤٹ کریں اور ایپ کا استعمال جاری رکھنے کے لیے دوبارہ لاگ ان کریں۔"
+
diff --git a/appnav/src/main/res/values-uz/translations.xml b/appnav/src/main/res/values-uz/translations.xml
new file mode 100644
index 00000000000..a76aaaed927
--- /dev/null
+++ b/appnav/src/main/res/values-uz/translations.xml
@@ -0,0 +1,6 @@
+
+
+ "Tizmdan chiqish va yangilash"
+ "%1$s endi eski protokolni qoʻllab-quvvatlamaydi. Iltimos, ilovadan foydalanishni davom ettirish uchun tizimdan chiqing va qayta kiring."
+ "Sizning uy serveringiz endi eski protokolni qoʻllab-quvvatlamaydi. Iltimos, ilovadan foydalanishni davom ettirish uchun tizimdan qayta chiqib-kiring."
+
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
index bbaa1965202..77fcbae14ef 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -17,24 +18,33 @@ import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth.assertThat
-import io.element.android.appnav.di.RoomComponentFactory
+import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.appnav.room.RoomNavigationTarget
+import io.element.android.appnav.room.joined.FakeJoinedRoomLoadedFlowNodeCallback
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
+import io.element.android.features.forward.api.ForwardEntryPoint
+import io.element.android.features.forward.test.FakeForwardEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
+import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.childNode
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
+import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
+import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+@RunWith(RobolectricTestRunner::class)
class JoinedRoomLoadedFlowNodeTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@@ -42,35 +52,26 @@ class JoinedRoomLoadedFlowNodeTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
- private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder {
- var buildContext: BuildContext? = null
+ private class FakeMessagesEntryPoint : MessagesEntryPoint {
var nodeId: String? = null
var parameters: MessagesEntryPoint.Params? = null
var callback: MessagesEntryPoint.Callback? = null
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder {
- this.buildContext = buildContext
- return this
- }
-
- override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: MessagesEntryPoint.Params,
+ callback: MessagesEntryPoint.Callback,
+ ): Node {
parameters = params
- return this
- }
-
- override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
this.callback = callback
- return this
- }
-
- override fun build(): Node {
- return node(buildContext!!) {}.also {
+ return node(buildContext) {}.also {
nodeId = it.id
}
}
}
- private class FakeRoomComponentFactory : RoomComponentFactory {
+ private class FakeRoomGraphFactory : RoomGraphFactory {
override fun create(room: JoinedRoom): Any {
return Unit
}
@@ -79,22 +80,26 @@ class JoinedRoomLoadedFlowNodeTest {
private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint {
var nodeId: String? = null
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder {
- return object : RoomDetailsEntryPoint.NodeBuilder {
- override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder {
- return this
- }
-
- override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder {
- return this
- }
-
- override fun build(): Node {
- return node(buildContext) {}.also {
- nodeId = it.id
- }
- }
- }
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: RoomDetailsEntryPoint.Params,
+ callback: RoomDetailsEntryPoint.Callback,
+ ) = node(buildContext) {}.also {
+ nodeId = it.id
+ }
+ }
+
+ private class FakeSpaceEntryPoint : SpaceEntryPoint {
+ var nodeId: String? = null
+
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: SpaceEntryPoint.Inputs,
+ callback: SpaceEntryPoint.Callback,
+ ) = node(buildContext) {}.also {
+ nodeId = it.id
}
}
@@ -102,27 +107,32 @@ class JoinedRoomLoadedFlowNodeTest {
plugins: List,
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
- activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
+ spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
+ forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
+ activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
+ matrixClient: FakeMatrixClient = FakeMatrixClient(),
) = JoinedRoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null),
plugins = plugins,
messagesEntryPoint = messagesEntryPoint,
roomDetailsEntryPoint = roomDetailsEntryPoint,
+ spaceEntryPoint = spaceEntryPoint,
+ forwardEntryPoint = forwardEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(),
- sessionCoroutineScope = this,
- roomComponentFactory = FakeRoomComponentFactory(),
- matrixClient = FakeMatrixClient(),
+ sessionCoroutineScope = backgroundScope,
+ roomGraphFactory = FakeRoomGraphFactory(),
+ matrixClient = matrixClient,
activeRoomsHolder = activeRoomsHolder,
)
@Test
- fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
+ fun `given a room flow node when initialized then it loads messages entry point if room is not space`() = runTest {
// GIVEN
- val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
+ val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = false)))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
- val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
+ val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode(
- plugins = listOf(inputs),
+ plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint,
)
// WHEN
@@ -135,21 +145,41 @@ class JoinedRoomLoadedFlowNodeTest {
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
}
+ @Test
+ fun `given a room flow node when initialized then it loads space entry point if room is space`() = runTest {
+ // GIVEN
+ val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = true)))
+ val spaceEntryPoint = FakeSpaceEntryPoint()
+ val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
+ val roomFlowNode = createJoinedRoomLoadedFlowNode(
+ plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
+ spaceEntryPoint = spaceEntryPoint,
+ )
+ // WHEN
+ val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
+
+ // THEN
+ assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Space)
+ roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Space, Lifecycle.State.CREATED)
+ val spaceNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Space)!!
+ assertThat(spaceNode.id).isEqualTo(spaceEntryPoint.nodeId)
+ }
+
@Test
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
// GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
- val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
+ val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode(
- plugins = listOf(inputs),
+ plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
)
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN
- fakeMessagesEntryPoint.callback?.onRoomDetailsClick()
+ fakeMessagesEntryPoint.callback?.navigateToRoomDetails()
// THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
@@ -162,10 +192,10 @@ class JoinedRoomLoadedFlowNodeTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
- val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
- val activeRoomsHolder = ActiveRoomsHolder()
+ val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
+ val activeRoomsHolder = DefaultActiveRoomsHolder()
val roomFlowNode = createJoinedRoomLoadedFlowNode(
- plugins = listOf(inputs),
+ plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder,
@@ -185,12 +215,12 @@ class JoinedRoomLoadedFlowNodeTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
- val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
- val activeRoomsHolder = ActiveRoomsHolder().apply {
+ val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
+ val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
addRoom(room)
}
val roomFlowNode = createJoinedRoomLoadedFlowNode(
- plugins = listOf(inputs),
+ plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder,
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
index 6d0878d812d..73d55135fb8 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,16 +15,13 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.root.RootPresenter
import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
-import io.element.android.features.share.api.ShareService
-import io.element.android.features.share.test.FakeShareService
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
+import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.lambda.lambdaRecorder
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -43,26 +41,12 @@ class RootPresenterTest {
}
}
- @Test
- fun `present - check that share service is invoked`() = runTest {
- val lambda = lambdaRecorder { _ -> }
- val presenter = createRootPresenter(
- shareService = FakeShareService {
- lambda(it)
- }
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- lambda.assertions().isCalledOnce()
- }
- }
-
@Test
fun `present - passes app error state`() = runTest {
val presenter = createRootPresenter(
- appErrorService = DefaultAppErrorStateService().apply {
+ appErrorService = DefaultAppErrorStateService(
+ stringProvider = FakeStringProvider(),
+ ).apply {
showError("Bad news", "Something bad happened")
}
)
@@ -81,15 +65,15 @@ class RootPresenterTest {
}
private fun createRootPresenter(
- appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
- shareService: ShareService = FakeShareService {},
+ appErrorService: AppErrorStateService = DefaultAppErrorStateService(
+ stringProvider = FakeStringProvider(),
+ ),
): RootPresenter {
return RootPresenter(
crashDetectionPresenter = { aCrashDetectionState() },
rageshakeDetectionPresenter = { aRageshakeDetectionState() },
appErrorStateService = appErrorService,
analyticsService = FakeAnalyticsService(),
- shareService = shareService,
sdkMetadata = FakeSdkMetadata("sha")
)
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
index 2c143df095d..7309ca6ac26 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,8 +12,8 @@ import io.element.android.appnav.di.SyncOrchestrator
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncState
-import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService
+import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -385,9 +386,11 @@ class SyncOrchestratorTest {
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
- matrixClient = FakeMatrixClient(syncService = syncService, sessionCoroutineScope = backgroundScope),
+ syncService = syncService,
+ sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
dispatchers = testCoroutineDispatchers(),
+ analyticsService = FakeAnalyticsService(),
)
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
index 47237d5f779..56c20f7a1df 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,12 +11,14 @@ package io.element.android.appnav.di
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
-import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
+import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -117,12 +120,17 @@ class MatrixSessionCacheTest {
}
private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory {
- override fun create(matrixClient: MatrixClient): SyncOrchestrator {
+ override fun create(
+ syncService: SyncService,
+ sessionCoroutineScope: CoroutineScope,
+ ): SyncOrchestrator {
return SyncOrchestrator(
- matrixClient,
+ syncService = syncService,
+ sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),
dispatchers = testCoroutineDispatchers(),
+ analyticsService = FakeAnalyticsService(),
)
}
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
index e95eb66cc38..bf673602c1e 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,11 +15,10 @@ import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver
-import io.element.android.libraries.deeplink.DeepLinkCreator
-import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.deeplink.DeeplinkParser
+import io.element.android.libraries.deeplink.api.DeeplinkData
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.AN_EVENT_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_THREAD_ID
@@ -46,15 +46,11 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent root`() {
- val sut = createIntentResolver()
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Root(A_SESSION_ID)
+ )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
- sessionId = A_SESSION_ID,
- roomId = null,
- threadId = null,
- )
- .toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -68,15 +64,16 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent room`() {
- val sut = createIntentResolver()
- val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
- action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
+ eventId = null,
)
- .toUri()
+ )
+ val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
+ action = Intent.ACTION_VIEW
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -85,6 +82,7 @@ class IntentResolverTest {
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
+ eventId = null,
)
)
)
@@ -92,15 +90,68 @@ class IntentResolverTest {
@Test
fun `test resolve navigation intent thread`() {
- val sut = createIntentResolver()
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ threadId = A_THREAD_ID,
+ eventId = null,
+ )
+ )
+ val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ }
+ val result = sut.resolve(intent)
+ assertThat(result).isEqualTo(
+ ResolvedIntent.Navigation(
+ deeplinkData = DeeplinkData.Room(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ threadId = A_THREAD_ID,
+ eventId = null,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `test resolve navigation intent event`() {
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ threadId = null,
+ eventId = AN_EVENT_ID,
+ )
+ )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
- data = DeepLinkCreator().room(
+ }
+ val result = sut.resolve(intent)
+ assertThat(result).isEqualTo(
+ ResolvedIntent.Navigation(
+ deeplinkData = DeeplinkData.Room(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ threadId = null,
+ eventId = AN_EVENT_ID,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `test resolve navigation intent thread and event`() {
+ val sut = createIntentResolver(
+ deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
+ eventId = AN_EVENT_ID,
)
- .toUri()
+ )
+ val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
+ action = Intent.ACTION_VIEW
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -109,6 +160,7 @@ class IntentResolverTest {
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
+ eventId = AN_EVENT_ID,
)
)
)
@@ -117,7 +169,7 @@ class IntentResolverTest {
@Test
fun `test resolve oidc`() {
val sut = createIntentResolver(
- oidcIntentResolverResult = { OidcAction.GoBack },
+ oidcIntentResolverResult = { OidcAction.GoBack() },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
@@ -126,7 +178,7 @@ class IntentResolverTest {
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
- oidcAction = OidcAction.GoBack
+ oidcAction = OidcAction.GoBack()
)
)
}
@@ -240,12 +292,13 @@ class IntentResolverTest {
}
private fun createIntentResolver(
+ deeplinkParserResult: DeeplinkData? = null,
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
): IntentResolver {
return IntentResolver(
- deeplinkParser = DeeplinkParser(),
+ deeplinkParser = { deeplinkParserResult },
loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult,
),
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateMappingTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateMappingTest.kt
index e965d15c6e2..50b9df5820c 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateMappingTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/AnalyticsVerificationStateMappingTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
index d86c46f1598..849dfa85b2a 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.push.api.PushService
+import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
@@ -41,7 +43,6 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
-import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@@ -114,7 +115,9 @@ class LoggedInPresenterTest {
encryptionService = encryptionService,
),
syncService = FakeSyncService(initialSyncState = SyncState.Running),
- pushService = FakePushService(),
+ pushService = FakePushService(
+ ensurePusherIsRegisteredResult = { Result.success(Unit) },
+ ),
sessionVerificationService = verificationService,
analyticsService = analyticsService,
encryptionService = encryptionService,
@@ -138,10 +141,10 @@ class LoggedInPresenterTest {
@Test
fun `present - ensure default pusher is not registered if session is not verified`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
+ val lambda = lambdaRecorder> {
Result.success(Unit)
}
- val pushService = createFakePushService(registerWithLambda = lambda)
+ val pushService = createFakePushService(ensurePusherIsRegisteredResult = lambda)
val verificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified
)
@@ -152,21 +155,18 @@ class LoggedInPresenterTest {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java)
- lambda.assertions()
- .isNeverCalled()
+ lambda.assertions().isNeverCalled()
}
}
@Test
fun `present - ensure default pusher is registered with default provider`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
+ val lambda = lambdaRecorder> { Result.success(Unit) }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createFakePushService(
- registerWithLambda = lambda,
+ ensurePusherIsRegisteredResult = lambda,
)
createLoggedInPresenter(
pushService = pushService,
@@ -179,27 +179,17 @@ class LoggedInPresenterTest {
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
lambda.assertions()
.isCalledOnce()
- .with(
- // MatrixClient
- any(),
- // PushProvider with highest priority (lower index)
- value(pushService.getAvailablePushProviders()[0]),
- // First distributor
- value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
- )
}
}
@Test
fun `present - ensure default pusher is registered with default provider - fail to register`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.failure(AN_EXCEPTION)
- }
+ val lambda = lambdaRecorder> { Result.failure(AN_EXCEPTION) }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createFakePushService(
- registerWithLambda = lambda,
+ ensurePusherIsRegisteredResult = lambda,
)
createLoggedInPresenter(
pushService = pushService,
@@ -212,83 +202,24 @@ class LoggedInPresenterTest {
assertThat(finalState.pusherRegistrationState.isFailure()).isTrue()
lambda.assertions()
.isCalledOnce()
- .with(
- // MatrixClient
- any(),
- // PushProvider with highest priority (lower index)
- value(pushService.getAvailablePushProviders()[0]),
- // First distributor
- value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
- )
- }
- }
-
- @Test
- fun `present - ensure current provider is registered with current distributor`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
- val sessionVerificationService = FakeSessionVerificationService(
- initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
- )
- val distributor = Distributor("aDistributorValue1", "aDistributorName1")
- val pushProvider = FakePushProvider(
- index = 0,
- name = "aFakePushProvider0",
- distributors = listOf(
- Distributor("aDistributorValue0", "aDistributorName0"),
- distributor,
- ),
- currentDistributor = { distributor },
- )
- val pushService = createFakePushService(
- pushProvider1 = pushProvider,
- currentPushProvider = { pushProvider },
- registerWithLambda = lambda,
- )
- createLoggedInPresenter(
- pushService = pushService,
- sessionVerificationService = sessionVerificationService,
- matrixClient = FakeMatrixClient(
- accountManagementUrlResult = { Result.success(null) },
- ),
- ).test {
- val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
- lambda.assertions()
- .isCalledOnce()
- .with(
- // MatrixClient
- any(),
- // Current push provider
- value(pushProvider),
- // Current distributor
- value(distributor),
- )
+ // Reset the error and do not show again
+ finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
+ val lastState = awaitItem()
+ assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
+ assertThat(lastState.ignoreRegistrationError).isFalse()
}
}
@Test
- fun `present - if current push provider does not have current distributor, the first one is used`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
+ fun `present - ensure default pusher is registered with default provider - fail to register - do not show again`() = runTest {
+ val lambda = lambdaRecorder> { Result.failure(AN_EXCEPTION) }
+ val setIgnoreRegistrationErrorLambda = lambdaRecorder { _, _ -> }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
- val pushProvider = FakePushProvider(
- index = 0,
- name = "aFakePushProvider0",
- distributors = listOf(
- Distributor("aDistributorValue0", "aDistributorName0"),
- Distributor("aDistributorValue1", "aDistributorName1"),
- ),
- currentDistributor = { null },
- )
val pushService = createFakePushService(
- pushProvider0 = pushProvider,
- currentPushProvider = { pushProvider },
- registerWithLambda = lambda,
+ ensurePusherIsRegisteredResult = lambda,
+ setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
)
createLoggedInPresenter(
pushService = pushService,
@@ -298,72 +229,9 @@ class LoggedInPresenterTest {
),
).test {
val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
+ assertThat(finalState.pusherRegistrationState.isFailure()).isTrue()
lambda.assertions()
.isCalledOnce()
- .with(
- // MatrixClient
- any(),
- // PushProvider with highest priority (lower index)
- value(pushService.getAvailablePushProviders()[0]),
- // First distributor
- value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
- )
- }
- }
-
- @Test
- fun `present - if current push provider does not have distributors, nothing happen`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
- val sessionVerificationService = FakeSessionVerificationService(
- initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
- )
- val pushProvider = FakePushProvider(
- index = 0,
- name = "aFakePushProvider0",
- distributors = emptyList(),
- )
- val pushService = createFakePushService(
- pushProvider0 = pushProvider,
- currentPushProvider = { pushProvider },
- registerWithLambda = lambda,
- )
- createLoggedInPresenter(
- pushService = pushService,
- sessionVerificationService = sessionVerificationService,
- ).test {
- val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.errorOrNull())
- .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
- lambda.assertions()
- .isNeverCalled()
- }
- }
-
- @Test
- fun `present - case no push provider available provider`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
- val sessionVerificationService = FakeSessionVerificationService(SessionVerifiedStatus.Verified)
- val setIgnoreRegistrationErrorLambda = lambdaRecorder { _, _ -> }
- val pushService = createFakePushService(
- pushProvider0 = null,
- pushProvider1 = null,
- registerWithLambda = lambda,
- setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
- )
- createLoggedInPresenter(
- pushService = pushService,
- sessionVerificationService = sessionVerificationService,
- ).test {
- val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.errorOrNull())
- .isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
- lambda.assertions()
- .isNeverCalled()
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
skipItems(1)
@@ -381,95 +249,6 @@ class LoggedInPresenterTest {
}
}
- @Test
- fun `present - case one push provider but no distributor available`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
- val selectPushProviderLambda = lambdaRecorder { _, _ -> }
- val sessionVerificationService = FakeSessionVerificationService(
- initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
- )
- val pushProvider = FakePushProvider(
- index = 0,
- name = "aFakePushProvider",
- distributors = emptyList(),
- )
- val pushService = createFakePushService(
- pushProvider0 = pushProvider,
- pushProvider1 = null,
- registerWithLambda = lambda,
- selectPushProviderLambda = selectPushProviderLambda,
- )
- createLoggedInPresenter(
- pushService = pushService,
- sessionVerificationService = sessionVerificationService,
- ).test {
- val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.errorOrNull())
- .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
- lambda.assertions()
- .isNeverCalled()
- selectPushProviderLambda.assertions()
- .isCalledOnce()
- .with(
- // SessionId
- value(A_SESSION_ID),
- // PushProvider
- value(pushProvider),
- )
- // Reset the error
- finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
- val lastState = awaitItem()
- assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
- }
- }
-
- @Test
- fun `present - case two push providers but first one does not have distributor - second one will be used`() = runTest {
- val lambda = lambdaRecorder> { _, _, _ ->
- Result.success(Unit)
- }
- val sessionVerificationService = FakeSessionVerificationService(
- initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
- )
- val pushProvider0 = FakePushProvider(
- index = 0,
- name = "aFakePushProvider0",
- distributors = emptyList(),
- )
- val distributor = Distributor("aDistributorValue1", "aDistributorName1")
- val pushProvider1 = FakePushProvider(
- index = 1,
- name = "aFakePushProvider1",
- distributors = listOf(distributor),
- )
- val pushService = createFakePushService(
- pushProvider0 = pushProvider0,
- pushProvider1 = pushProvider1,
- registerWithLambda = lambda,
- )
- createLoggedInPresenter(
- pushService = pushService,
- sessionVerificationService = sessionVerificationService,
- matrixClient = FakeMatrixClient(
- accountManagementUrlResult = { Result.success(null) },
- ),
- ).test {
- val finalState = awaitFirstItem()
- assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
- lambda.assertions().isCalledOnce()
- .with(
- // MatrixClient
- any(),
- // PushProvider with the distributor
- value(pushProvider1),
- // First distributor of second push provider
- value(distributor),
- )
- }
- }
-
private fun createFakePushService(
pushProvider0: PushProvider? = FakePushProvider(
index = 0,
@@ -483,16 +262,16 @@ class LoggedInPresenterTest {
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
currentDistributor = { null },
),
- registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result = { _, _, _ ->
+ ensurePusherIsRegisteredResult: () -> Result = {
Result.success(Unit)
},
selectPushProviderLambda: (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
- currentPushProvider: () -> PushProvider? = { null },
+ currentPushProvider: (SessionId) -> PushProvider? = { null },
setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
): PushService {
return FakePushService(
availablePushProviders = listOfNotNull(pushProvider0, pushProvider1),
- registerWithLambda = registerWithLambda,
+ ensurePusherIsRegisteredResult = ensurePusherIsRegisteredResult,
currentPushProvider = currentPushProvider,
selectPushProviderLambda = selectPushProviderLambda,
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
@@ -501,22 +280,16 @@ class LoggedInPresenterTest {
@Test
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
- // The migration will be forced if:
- // - The user is not using the native sliding sync
- // - The sliding sync proxy is no longer supported
- // - The native sliding sync is supported
+ // The migration will be forced if the user is not using the native sliding sync
val matrixClient = FakeMatrixClient(
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
- availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) },
)
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
-
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
-
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
}
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt
index 459abc64d4d..8e081bdad70 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt
index c117dfb8f76..3c5d8dc0761 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt
index 91e0b9f9681..c8810dff923 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt
new file mode 100644
index 00000000000..40778ae3532
--- /dev/null
+++ b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.appnav.room.joined
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.permalink.PermalinkData
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
+ override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError()
+ override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
+ override fun navigateToGlobalNotificationSettings() = lambdaError()
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 0e585a3d6bc..ef12d98fd33 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,29 +1,24 @@
+import org.gradle.accessors.dm.LibrariesForLibs
+
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-buildscript {
- dependencies {
- classpath(libs.kotlin.gradle.plugin)
- classpath(libs.gms.google.services)
- }
-}
-
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("io.element.android-root")
+ alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) apply false
- alias(libs.plugins.anvil) apply false
- alias(libs.plugins.kotlin.jvm) apply false
- alias(libs.plugins.kapt) apply false
alias(libs.plugins.dependencycheck) apply false
+ alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.dependencyanalysis)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
@@ -35,6 +30,8 @@ tasks.register("clean").configure {
delete(rootProject.layout.buildDirectory)
}
+private val ktLintVersion = the().versions.ktlint.get()
+
allprojects {
// Detekt
apply {
@@ -49,7 +46,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
- detektPlugins("io.nlopez.compose.rules:detekt:0.4.22")
+ detektPlugins("io.nlopez.compose.rules:detekt:0.4.27")
detektPlugins(project(":tests:detekt-rules"))
}
@@ -64,14 +61,12 @@ allprojects {
// See https://github.com/JLLeitschuh/ktlint-gradle#configuration
configure {
- // See https://github.com/pinterest/ktlint/releases/
- // TODO Regularly check for new version here ^
- version.set("1.1.1")
- android.set(true)
- ignoreFailures.set(false)
- enableExperimentalRules.set(true)
+ version = ktLintVersion
+ android = true
+ ignoreFailures = false
+ enableExperimentalRules = true
// display the corresponding rule
- verbose.set(true)
+ verbose = true
reporters {
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
// To have XML report for Danger
@@ -102,6 +97,8 @@ allprojects {
// Fix compilation warning for annotations
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
freeCompilerArgs.add("-Xannotation-default-target=first-only")
+ // Opt-in to context receivers
+ freeCompilerArgs.add("-Xcontext-parameters")
}
}
}
@@ -199,6 +196,21 @@ subprojects {
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
}
+// Make sure to delete old snapshot before recording new ones
+subprojects {
+ val screenshotsDir = File("${project.projectDir}/screenshots")
+ val removeOldScreenshotsTask = tasks.register("removeOldScreenshots") {
+ onlyIf { screenshotsDir.exists() }
+ doFirst {
+ println("Delete previous screenshots located at $screenshotsDir\n")
+ screenshotsDir.deleteRecursively()
+ }
+ }
+ tasks.findByName("recordRoborazzi")?.dependsOn(removeOldScreenshotsTask)
+ tasks.findByName("recordRoborazziDebug")?.dependsOn(removeOldScreenshotsTask)
+ tasks.findByName("recordRoborazziRelease")?.dependsOn(removeOldScreenshotsTask)
+}
+
subprojects {
tasks.withType().configureEach {
compilerOptions {
diff --git a/anvilcodegen/.gitignore b/codegen/.gitignore
similarity index 100%
rename from anvilcodegen/.gitignore
rename to codegen/.gitignore
diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts
new file mode 100644
index 00000000000..5c71c170bf4
--- /dev/null
+++ b/codegen/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+}
+
+dependencies {
+ implementation(projects.annotations)
+ implementation(libs.metro.runtime)
+ implementation(libs.kotlin.compiler)
+ implementation(libs.kotlinpoet)
+ implementation(libs.ksp.plugin)
+ implementation(libs.kotlinpoet.ksp)
+}
diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
similarity index 77%
rename from anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
rename to codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
index 2511e0008d3..7ddf80b92ca 100644
--- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
+++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.anvilcodegen
+package io.element.android.codegen
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getConstructors
@@ -19,7 +20,6 @@ import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate
-import com.squareup.anvil.annotations.ContributesTo
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
@@ -30,13 +30,14 @@ import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
-import dagger.Binds
-import dagger.Module
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import dagger.multibindings.IntoMap
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.IntoMap
+import dev.zacsweers.metro.Origin
+import io.element.android.annotations.ContributesNode
import org.jetbrains.kotlin.name.FqName
class ContributesNodeProcessor(
@@ -72,15 +73,16 @@ class ContributesNodeProcessor(
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
val modulePackage = ksClass.packageName.asString()
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
+ val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val content = FileSpec.builder(
packageName = modulePackage,
fileName = moduleClassName,
)
.addType(
- TypeSpec.classBuilder(moduleClassName)
- .addModifiers(KModifier.ABSTRACT)
- .addAnnotation(Module::class)
- .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
+ TypeSpec.interfaceBuilder(moduleClassName)
+ .addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
+ .addAnnotation(BindingContainer::class)
+ .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
.addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
.addModifiers(KModifier.ABSTRACT)
@@ -90,7 +92,7 @@ class ContributesNodeProcessor(
.addAnnotation(IntoMap::class)
.addAnnotation(
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
- "%T::class",
+ CLASS_PLACEHOLDER,
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
).build()
)
@@ -103,7 +105,7 @@ class ContributesNodeProcessor(
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
- aggregating = true,
+ aggregating = false,
ksClass.containingFile!!
),
)
@@ -113,23 +115,23 @@ class ContributesNodeProcessor(
private fun generateFactory(ksClass: KSClassDeclaration) {
val generatedPackage = ksClass.packageName.asString()
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
- val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) }
- val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty()
- if (constructor == null || assistedParameters.size != 2) {
+ val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
+ val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
+ if (assistedParameters.size != 2) {
error(
- "${ksClass.qualifiedName} must have an @AssistedInject constructor with 2 @Assisted parameters",
+ "${ksClass.qualifiedName?.asString()} must have a constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
)
}
val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name?.asString() != "buildContext") {
error(
- "${ksClass.qualifiedName} @Assisted parameter must be named buildContext",
+ "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named buildContext",
)
}
val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name?.asString() != "plugins") {
error(
- "${ksClass.qualifiedName} @Assisted parameter must be named plugins",
+ "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named plugins",
)
}
@@ -140,6 +142,7 @@ class ContributesNodeProcessor(
.addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
+ .addAnnotation(AnnotationSpec.builder(Origin::class).addMember("%T::class", nodeClassName).build())
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
@@ -156,13 +159,14 @@ class ContributesNodeProcessor(
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
- aggregating = true,
+ aggregating = false,
ksClass.containingFile!!
),
)
}
companion object {
+ private const val CLASS_PLACEHOLDER = "%T::class"
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt
similarity index 86%
rename from anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt
rename to codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt
index ec6ad5958e3..8412fbf847a 100644
--- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt
+++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessorProvider.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.anvilcodegen
+package io.element.android.codegen
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
diff --git a/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
new file mode 100644
index 00000000000..2d18fdfd5c1
--- /dev/null
+++ b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
@@ -0,0 +1 @@
+io.element.android.codegen.ContributesNodeProcessorProvider
diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md
index b8141daef82..6fb5229eeb8 100644
--- a/docs/_developer_onboarding.md
+++ b/docs/_developer_onboarding.md
@@ -33,7 +33,7 @@
This doc is a quick introduction about the project and its architecture.
-It's aim is to help new developers to understand the overall project and where to start developing.
+Its aim is to help new developers to understand the overall project and where to start developing.
Other useful documentation:
@@ -157,6 +157,8 @@ Troubleshooting:
- If you get the error `Unsupported class file major version `, try changing your JVM version by setting
`JAVA_HOME` and, if building via Android Studio, "File | Settings | Build, Execution, Deployment | Build Tools | Gradle | Gradle JDK".
+You can switch back to using the published version of the SDK by deleting `libraries/rustsdk/matrix-rust-sdk.aar`.
+
### The Android project
The project should compile out of the box.
@@ -247,8 +249,7 @@ Main libraries and frameworks used in this application:
- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please
watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx!
-- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil). Please
- watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil!
+- Dependency injection: [Metro](https://zacsweers.github.io/metro/latest/)
- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule)
Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/)
@@ -259,7 +260,7 @@ Here are the main points:
2. Views are compose first
3. Presenters are also compose first, and have a single `present(): State` method. It's using the power of compose-runtime/compiler.
4. The point of connection between a `View` and a `Presenter` is a `Node`.
-5. A `Node` is also responsible for managing Dagger components if any.
+5. A `Node` is also responsible for managing DI graph if any, see for instance `LoggedInAppScopeFlowNode`.
6. A `ParentNode` has some children `Node` and only know about them.
7. This is a single activity full compose application. The `MainActivity` is responsible for holding and configuring the `RootNode`.
8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed.
@@ -421,7 +422,7 @@ Rageshake can be very useful to get logs from a release version of the applicati
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI
will detect this String and will warn the user about it. (TODO Not supported yet!)
-- Very occasionally the gradle cache misbehaves and causes problems with Dagger. Try building with `--no-build-cache` if Dagger isn't behaving how you expect.
+- Very occasionally the gradle cache misbehaves and causes problems with code generation. Adding `--no-build-cache` to the `gradlew` command line can help to fix compilation issue.
## Happy coding!
diff --git a/docs/migration_to_metro.md b/docs/migration_to_metro.md
new file mode 100644
index 00000000000..23c5db97093
--- /dev/null
+++ b/docs/migration_to_metro.md
@@ -0,0 +1,15 @@
+# Migration to Metro
+
+The dependency injection library is now [Metro](https://zacsweers.github.io/metro/latest/). It replaces both Dagger and Anvil.
+
+Migration of the current Element X code has been performed in https://github.com/element-hq/element-x-android/pull/5253.
+
+To migrate other existing code you will need to:
+
+- replace `setupAnvil()` with `setupDependencyInjection()` in your `build.gradle.kts` files
+- replace the Dagger and Anvil imports with Metro ones
+- move the `@Inject` apply to the constructor to the class itself (only applicable if there is only one primary constructor
+- replace `@AssistedInject` with `@Inject`
+- replace `@Module` with `@BindingContainer`
+
+This should help to migrate your existing code.
diff --git a/docs/notifications.md b/docs/notifications.md
index 612b8785b89..5f67f88713d 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -149,7 +149,7 @@ That is why clients are able to **process the push rules client side** to decide
As seen previously, App developers don't directly send a push to the end user's device, they use a Push Provider as intermediary. So technically this intermediary is able to read the content of what is sent.
-App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to it's server in order to generate a local notification.
+App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to its server in order to generate a local notification.
### Background processing limitations
diff --git a/enterprise b/enterprise
index b7ababb9537..6207ddc1cb7 160000
--- a/enterprise
+++ b/enterprise
@@ -1 +1 @@
-Subproject commit b7ababb9537da8bec254b8ed00b5a4122e9f3e3b
+Subproject commit 6207ddc1cb7dac7fdb6212c0158497a1d9752c75
diff --git a/fastlane/metadata/android/en-US/changelogs/202507010.txt b/fastlane/metadata/android/en-US/changelogs/202507010.txt
new file mode 100644
index 00000000000..42a042fb061
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202507010.txt
@@ -0,0 +1,2 @@
+Main changes in this version: improvements and bug fixes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/202508000.txt b/fastlane/metadata/android/en-US/changelogs/202508000.txt
new file mode 100644
index 00000000000..46897eb2fb2
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508000.txt
@@ -0,0 +1,2 @@
+Main changes in this version: start support for room v12, bug fixes and general improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202508010.txt b/fastlane/metadata/android/en-US/changelogs/202508010.txt
new file mode 100644
index 00000000000..e96d596b06a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508010.txt
@@ -0,0 +1,4 @@
+Main changes in this version:
+- Fixes for the room v12 changes.
+- Several bug fixes, centered on media and calls.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202508020.txt b/fastlane/metadata/android/en-US/changelogs/202508020.txt
new file mode 100644
index 00000000000..4da3e650499
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508020.txt
@@ -0,0 +1,3 @@
+Main changes in this version:
+- Fix a bug with notifications being incorrectly dropped.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202508030.txt b/fastlane/metadata/android/en-US/changelogs/202508030.txt
new file mode 100644
index 00000000000..8955ade6804
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508030.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202508040.txt b/fastlane/metadata/android/en-US/changelogs/202508040.txt
new file mode 100644
index 00000000000..ac9a4fb45b5
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202508040.txt
@@ -0,0 +1,2 @@
+Main changes in this version: you can now create shortcuts to your recent conversations, several bug fixes related to media processing and downloading.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/202509000.txt b/fastlane/metadata/android/en-US/changelogs/202509000.txt
new file mode 100644
index 00000000000..97562251c51
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202509000.txt
@@ -0,0 +1,2 @@
+Main changes in this version: improved timeline loading times, you can now create shortcuts to your recent conversations, several bug fixes related to media processing and downloading.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/202509010.txt b/fastlane/metadata/android/en-US/changelogs/202509010.txt
new file mode 100644
index 00000000000..8955ade6804
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202509010.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202509020.txt b/fastlane/metadata/android/en-US/changelogs/202509020.txt
new file mode 100644
index 00000000000..8955ade6804
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202509020.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202510000.txt b/fastlane/metadata/android/en-US/changelogs/202510000.txt
new file mode 100644
index 00000000000..e30ec573cc9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202510000.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Spaces!
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202510010.txt b/fastlane/metadata/android/en-US/changelogs/202510010.txt
new file mode 100644
index 00000000000..3c916fed888
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202510010.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes around notifications and UX improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202511000.txt b/fastlane/metadata/android/en-US/changelogs/202511000.txt
new file mode 100644
index 00000000000..8afd7460fc0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202511000.txt
@@ -0,0 +1,2 @@
+Main changes in this version: fixes an issue that prevented Element Call notifications from being displayed sometimes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/202511020.txt b/fastlane/metadata/android/en-US/changelogs/202511020.txt
new file mode 100644
index 00000000000..a4b397f1bb8
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202511020.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/202511030.txt b/fastlane/metadata/android/en-US/changelogs/202511030.txt
new file mode 100644
index 00000000000..a4b397f1bb8
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202511030.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index d0b94798861..862c235c729 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -22,7 +22,7 @@ Choose where to host your data - from any public server (the largest free server
Use Element everywhere. Stay in touch wherever you are with fully synchronised message history across all your devices, including on the web at https://app.element.io
Element X is our next-generation app
-If you’re using the original Element app, it’s time to try Element X! It’s faster, easier to use, and more powerful than the original app. It’s better in every way and we’re adding new features all the time.
+If you’re using the previous-generation Element Classic app, it’s time to try Element X! It’s faster, easier to use, and more powerful than the classic app. It’s better in every way and we’re adding new features all the time.
The application requires the android.permission.REQUEST_INSTALL_PACKAGES permission to enable the installation of applications received as attachments, ensuring seamless and convenient access to new software within the app.
diff --git a/features/analytics/api/build.gradle.kts b/features/analytics/api/build.gradle.kts
index 4bf21e265ea..5c7f0b223a9 100644
--- a/features/analytics/api/build.gradle.kts
+++ b/features/analytics/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt
index 404bae44d1b..5d5c1ba2860 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,4 +10,4 @@ package io.element.android.features.analytics.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
-interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
+fun interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt
index 74d26c4f040..4181f52ba87 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
index 4cb3767c72d..20a180446c9 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt
index 647b2057227..02e07a86a2f 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
index 70461f22d31..e91c77079fa 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/api/src/main/res/values-de/translations.xml b/features/analytics/api/src/main/res/values-de/translations.xml
index 30fd8cd7e5d..bf3584e3b4c 100644
--- a/features/analytics/api/src/main/res/values-de/translations.xml
+++ b/features/analytics/api/src/main/res/values-de/translations.xml
@@ -1,7 +1,7 @@
"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."
- "Sie können unsere Bedingungen %1$s lesen."
+ "Weitere Informationen findest du %1$s.""hier""Analysedaten teilen"
diff --git a/features/analytics/api/src/main/res/values-ko/translations.xml b/features/analytics/api/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..809fc084f71
--- /dev/null
+++ b/features/analytics/api/src/main/res/values-ko/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오."
+ "모든 이용 약관은 %1$s 에서 확인하실 수 있습니다."
+ "여기"
+ "분석 데이터 공유"
+
diff --git a/features/analytics/api/src/main/res/values-pt-rBR/translations.xml b/features/analytics/api/src/main/res/values-pt-rBR/translations.xml
index ab649cc9fff..01fe907d5fc 100644
--- a/features/analytics/api/src/main/res/values-pt-rBR/translations.xml
+++ b/features/analytics/api/src/main/res/values-pt-rBR/translations.xml
@@ -3,5 +3,5 @@
"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas.""Você pode ler todos os nossos termos %1$s.""aqui"
- "Compartilhar dados de utilização"
+ "Compartilhar dados analíticos"
diff --git a/features/analytics/impl/build.gradle.kts b/features/analytics/impl/build.gradle.kts
index f78574e51dc..cdb172f8881 100644
--- a/features/analytics/impl/build.gradle.kts
+++ b/features/analytics/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,7 +18,7 @@ android {
namespace = "io.element.android.features.analytics.impl"
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
implementation(projects.libraries.androidutils)
@@ -30,13 +32,7 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.browser)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.mockk)
+ testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
- testImplementation(projects.tests.testutils)
}
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt
index ba1073d480c..545f306a681 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,16 +15,17 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
-import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
-class AnalyticsOptInNode @AssistedInject constructor(
+@AssistedInject
+class AnalyticsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: AnalyticsOptInPresenter,
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
index 39b99a9257c..d7bdb85e359 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,6 +10,7 @@ package io.element.android.features.analytics.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.architecture.Presenter
@@ -16,9 +18,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class AnalyticsOptInPresenter @Inject constructor(
+@Inject
+class AnalyticsOptInPresenter(
private val buildMeta: BuildMeta,
private val analyticsService: AnalyticsService,
) : Presenter {
@@ -26,7 +28,7 @@ class AnalyticsOptInPresenter @Inject constructor(
override fun present(): AnalyticsOptInState {
val localCoroutineScope = rememberCoroutineScope()
- fun handleEvents(event: AnalyticsOptInEvents) {
+ fun handleEvent(event: AnalyticsOptInEvents) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
@@ -38,7 +40,7 @@ class AnalyticsOptInPresenter @Inject constructor(
return AnalyticsOptInState(
applicationName = buildMeta.applicationName,
hasPolicyLink = AnalyticsConfig.POLICY_LINK.isNotEmpty(),
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
index a0913bdb4f5..b2ebd37e209 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
index e6917c237ee..30a396c1783 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
@@ -1,16 +1,16 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import javax.inject.Inject
-open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterProvider {
+open class AnalyticsOptInStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aAnalyticsOptInState(),
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
index a6ae7549448..4fc9ce5c6b3 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -29,13 +30,13 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
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.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.ClickableLinkText
-import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
@@ -89,10 +90,10 @@ private fun AnalyticsOptInHeader(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
- PageTitle(
- modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 60.dp, bottom = 28.dp),
title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName),
- subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
+ subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
iconStyle = BigIcon.Style.Default(CompoundIcons.Chart())
)
if (state.hasPolicyLink) {
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt
index 4903534159b..fb853879506 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,14 +10,13 @@ package io.element.android.features.analytics.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultAnalyticsEntryPoint @Inject constructor() : AnalyticsEntryPoint {
+class DefaultAnalyticsEntryPoint : AnalyticsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode(buildContext)
}
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt
index d1ddd57b2f3..32a3d5826af 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt
@@ -1,22 +1,23 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.analytics.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
-@Module
+@BindingContainer
interface AnalyticsModule {
@Binds
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
index 6904734f264..110cb98d411 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,6 +11,7 @@ package io.element.android.features.analytics.impl.preferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
@@ -18,9 +20,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class AnalyticsPreferencesPresenter @Inject constructor(
+@Inject
+class AnalyticsPreferencesPresenter(
private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta,
) : Presenter {
@@ -29,7 +31,7 @@ class AnalyticsPreferencesPresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope()
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
- fun handleEvents(event: AnalyticsOptInEvents) {
+ fun handleEvent(event: AnalyticsOptInEvents) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
@@ -39,7 +41,7 @@ class AnalyticsPreferencesPresenter @Inject constructor(
applicationName = buildMeta.applicationName,
isEnabled = isEnabled.value,
policyUrl = AnalyticsConfig.POLICY_LINK,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml
index 9f3bd7e6527..9fd4dbf0a8b 100644
--- a/features/analytics/impl/src/main/res/values-de/translations.xml
+++ b/features/analytics/impl/src/main/res/values-de/translations.xml
@@ -1,10 +1,10 @@
- "Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."
+ "Wir speichern oder profilieren keine personenbezogenen Daten.""Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."
- "Sie können unsere Bedingungen %1$s lesen."
+ "Weitere Informationen findest du %1$s.""hier"
- "Sie können dies jederzeit beenden"
+ "Du kannst diese Funktion jederzeit deaktivieren""Wir geben deine Daten nicht an Dritte weiter""Hilf uns %1$s zu verbessern"
diff --git a/features/analytics/impl/src/main/res/values-ko/translations.xml b/features/analytics/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..80dca721202
--- /dev/null
+++ b/features/analytics/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "개인 데이터는 기록하거나 프로파일링하지 않습니다."
+ "익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오."
+ "모든 이용 약관은 %1$s 에서 확인하실 수 있습니다."
+ "여기"
+ "이 기능을 언제든지 비활성화할 수 있습니다."
+ "우리는 귀하의 데이터를 제3자와 공유하지 않습니다."
+ "%1$s 개선하기"
+
diff --git a/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml b/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml
index b91ab362565..c9a53b8c9f2 100644
--- a/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml
@@ -1,6 +1,6 @@
- "Não registraremos nem criaremos perfil baseado em qualquer dado pessoal"
+ "Não iremos gravar ou personificar qualquer dado pessoal""Compartilhe dados de uso anônimos para nos ajudar a identificar problemas.""Você pode ler todos os nossos termos %1$s.""aqui"
diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
index e9319fd7bad..6521c7f597d 100644
--- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
+++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.kt
new file mode 100644
index 00000000000..0340bd3d68d
--- /dev/null
+++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/DefaultAnalyticsEntryPointTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.analytics.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.core.aBuildMeta
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultAnalyticsEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node creation`() {
+ val entryPoint = DefaultAnalyticsEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ AnalyticsOptInNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ AnalyticsOptInPresenter(
+ buildMeta = aBuildMeta(),
+ analyticsService = FakeAnalyticsService()
+ )
+ )
+ }
+ val result = entryPoint.createNode(parentNode, BuildContext.root(null))
+ assertThat(result).isInstanceOf(AnalyticsOptInNode::class.java)
+ }
+}
diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
index 8ba915f03c8..174935a1adf 100644
--- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
+++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/announcement/api/build.gradle.kts b/features/announcement/api/build.gradle.kts
new file mode 100644
index 00000000000..0c2f5bb7b92
--- /dev/null
+++ b/features/announcement/api/build.gradle.kts
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.announcement.api"
+}
diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt
new file mode 100644
index 00000000000..0bf35650a06
--- /dev/null
+++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.api
+
+enum class Announcement {
+ Space,
+ NewNotificationSound,
+}
diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt
new file mode 100644
index 00000000000..42c66aa29d5
--- /dev/null
+++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.api
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.Flow
+
+interface AnnouncementService {
+ suspend fun showAnnouncement(announcement: Announcement)
+
+ suspend fun onAnnouncementDismissed(announcement: Announcement)
+
+ fun announcementsToShowFlow(): Flow>
+
+ /**
+ * Use this composable to render the announcement UI in Fullscreen.
+ */
+ @Composable
+ fun Render(
+ modifier: Modifier,
+ )
+}
diff --git a/features/announcement/impl/build.gradle.kts b/features/announcement/impl/build.gradle.kts
new file mode 100644
index 00000000000..443e343e437
--- /dev/null
+++ b/features/announcement/impl/build.gradle.kts
@@ -0,0 +1,38 @@
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
+
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.announcement.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupDependencyInjection()
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.preferences.api)
+ implementation(projects.libraries.uiStrings)
+ api(projects.features.announcement.api)
+ implementation(libs.androidx.datastore.preferences)
+
+ testCommonDependencies(libs, true)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt
new file mode 100644
index 00000000000..508f1e44a01
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.map
+
+@Inject
+class AnnouncementPresenter(
+ private val announcementStore: AnnouncementStore,
+) : Presenter {
+ @Composable
+ override fun present(): AnnouncementState {
+ val showSpaceAnnouncement by remember {
+ announcementStore.announcementStatusFlow(Announcement.Space).map {
+ it == AnnouncementStatus.Show
+ }
+ }.collectAsState(false)
+ return AnnouncementState(
+ showSpaceAnnouncement = showSpaceAnnouncement,
+ )
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt
new file mode 100644
index 00000000000..e762dd607ff
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+data class AnnouncementState(
+ val showSpaceAnnouncement: Boolean,
+)
+
+fun anAnnouncementState(
+ showSpaceAnnouncement: Boolean = false,
+) = AnnouncementState(
+ showSpaceAnnouncement = showSpaceAnnouncement,
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt
new file mode 100644
index 00000000000..0e5c30178ca
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+
+@ContributesBinding(AppScope::class)
+class DefaultAnnouncementService(
+ private val announcementStore: AnnouncementStore,
+ private val announcementPresenter: Presenter,
+ private val spaceAnnouncementPresenter: Presenter,
+) : AnnouncementService {
+ override suspend fun showAnnouncement(announcement: Announcement) {
+ when (announcement) {
+ Announcement.Space -> showSpaceAnnouncement()
+ Announcement.NewNotificationSound -> {
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
+ }
+ }
+ }
+
+ override suspend fun onAnnouncementDismissed(announcement: Announcement) {
+ announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Shown)
+ }
+
+ override fun announcementsToShowFlow(): Flow> {
+ return combine(
+ announcementStore.announcementStatusFlow(Announcement.Space),
+ announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
+ ) { spaceAnnouncementStatus, newNotificationSoundStatus ->
+ buildList {
+ if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
+ add(Announcement.Space)
+ }
+ if (newNotificationSoundStatus == AnnouncementStatus.Show) {
+ add(Announcement.NewNotificationSound)
+ }
+ }
+ }
+ }
+
+ private suspend fun showSpaceAnnouncement() {
+ val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
+ if (currentValue == AnnouncementStatus.NeverShown) {
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ }
+ }
+
+ @Composable
+ override fun Render(modifier: Modifier) {
+ val announcementState = announcementPresenter.present()
+ Box(modifier = modifier.fillMaxSize()) {
+ AnimatedVisibility(
+ visible = announcementState.showSpaceAnnouncement,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ val spaceAnnouncementState = spaceAnnouncementPresenter.present()
+ SpaceAnnouncementView(
+ state = spaceAnnouncementState,
+ )
+ }
+ }
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt
new file mode 100644
index 00000000000..4cfc0732715
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.di
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
+import io.element.android.features.announcement.impl.AnnouncementPresenter
+import io.element.android.features.announcement.impl.AnnouncementState
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.libraries.architecture.Presenter
+
+@ContributesTo(AppScope::class)
+@BindingContainer
+interface AnnouncementModule {
+ @Binds
+ fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter
+
+ @Binds
+ fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt
new file mode 100644
index 00000000000..3b968d09a63
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+sealed interface SpaceAnnouncementEvents {
+ data object Continue : SpaceAnnouncementEvents
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt
new file mode 100644
index 00000000000..7c4bc7b5ebc
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.launch
+
+@Inject
+class SpaceAnnouncementPresenter(
+ private val announcementStore: AnnouncementStore,
+) : Presenter {
+ @Composable
+ override fun present(): SpaceAnnouncementState {
+ val localCoroutineScope = rememberCoroutineScope()
+
+ fun handleEvent(event: SpaceAnnouncementEvents) {
+ when (event) {
+ SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ }
+ }
+ }
+
+ return SpaceAnnouncementState(
+ eventSink = ::handleEvent,
+ )
+ }
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt
new file mode 100644
index 00000000000..9407fad8726
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+data class SpaceAnnouncementState(
+ val eventSink: (SpaceAnnouncementEvents) -> Unit
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt
new file mode 100644
index 00000000000..27f48cc7ede
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+open class SpaceAnnouncementStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aSpaceAnnouncementState(),
+ )
+}
+
+fun aSpaceAnnouncementState(
+ eventSink: (SpaceAnnouncementEvents) -> Unit = {},
+) = SpaceAnnouncementState(
+ eventSink = eventSink,
+)
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt
new file mode 100644
index 00000000000..e0759bc4901
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+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.compound.tokens.generated.CompoundIcons
+import io.element.android.features.announcement.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+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.pages.HeaderFooterPage
+import io.element.android.libraries.designsystem.components.BigIcon
+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.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.persistentListOf
+
+/**
+ * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
+ */
+@Composable
+fun SpaceAnnouncementView(
+ state: SpaceAnnouncementState,
+ modifier: Modifier = Modifier,
+) {
+ val eventSink = state.eventSink
+
+ fun onContinue() {
+ eventSink(SpaceAnnouncementEvents.Continue)
+ }
+
+ BackHandler(onBack = ::onContinue)
+ HeaderFooterPage(
+ modifier = modifier,
+ isScrollable = true,
+ contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
+ header = {
+ SpaceAnnouncementHeader()
+ },
+ content = {
+ SpaceAnnouncementContent(
+ modifier = Modifier.padding(horizontal = 8.dp),
+ )
+ },
+ footer = {
+ SpaceAnnouncementFooter(
+ onContinue = ::onContinue,
+ )
+ }
+ )
+}
+
+@Composable
+private fun SpaceAnnouncementHeader(
+ modifier: Modifier = Modifier,
+) {
+ IconTitleSubtitleMolecule(
+ modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
+ title = stringResource(id = R.string.screen_space_announcement_title),
+ showBetaLabel = true,
+ subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
+ iconStyle = BigIcon.Style.Default(
+ vectorIcon = CompoundIcons.WorkspaceSolid(),
+ usePrimaryTint = true,
+ ),
+ )
+}
+
+@Composable
+private fun SpaceAnnouncementContent(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ InfoListOrganism(
+ modifier = Modifier.fillMaxWidth(),
+ items = persistentListOf(
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item1),
+ iconVector = CompoundIcons.VisibilityOn(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item2),
+ iconVector = CompoundIcons.Email(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item3),
+ iconVector = CompoundIcons.Search(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item4),
+ iconVector = CompoundIcons.Explore(),
+ ),
+ InfoListItem(
+ message = stringResource(id = R.string.screen_space_announcement_item5),
+ iconVector = CompoundIcons.Leave(),
+ ),
+ ),
+ textStyle = ElementTheme.typography.fontBodyLgMedium,
+ iconTint = ElementTheme.colors.iconSecondary,
+ iconSize = 24.dp
+ )
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ text = stringResource(id = R.string.screen_space_announcement_notice),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Composable
+private fun SpaceAnnouncementFooter(
+ onContinue: () -> Unit,
+) {
+ ButtonColumnMolecule(
+ modifier = Modifier.padding(bottom = 8.dp)
+ ) {
+ Button(
+ text = stringResource(id = CommonStrings.action_continue),
+ onClick = onContinue,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview {
+ SpaceAnnouncementView(
+ state = state,
+ )
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt
new file mode 100644
index 00000000000..5f3dc7d0e40
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStatus.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+enum class AnnouncementStatus {
+ NeverShown,
+ Show,
+ Shown,
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt
new file mode 100644
index 00000000000..c818e90c4a1
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+import io.element.android.features.announcement.api.Announcement
+import kotlinx.coroutines.flow.Flow
+
+interface AnnouncementStore {
+ suspend fun setAnnouncementStatus(
+ announcement: Announcement,
+ status: AnnouncementStatus,
+ )
+
+ fun announcementStatusFlow(
+ announcement: Announcement,
+ ): Flow
+
+ suspend fun reset()
+}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt
new file mode 100644
index 00000000000..ad166e4ef5e
--- /dev/null
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
+private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
+
+@ContributesBinding(AppScope::class)
+class DefaultAnnouncementStore(
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
+) : AnnouncementStore {
+ private val store = preferenceDataStoreFactory.create("elementx_announcement")
+
+ override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
+ val key = announcement.toKey()
+ store.edit { prefs ->
+ prefs[key] = status.ordinal
+ }
+ }
+
+ override fun announcementStatusFlow(announcement: Announcement): Flow {
+ val key = announcement.toKey()
+ // For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
+ val defaultStatus = when (announcement) {
+ Announcement.Space -> AnnouncementStatus.NeverShown
+ Announcement.NewNotificationSound -> AnnouncementStatus.Shown
+ }
+ return store.data.map { prefs ->
+ val ordinal = prefs[key] ?: defaultStatus.ordinal
+ AnnouncementStatus.entries.getOrElse(ordinal) { defaultStatus }
+ }
+ }
+
+ override suspend fun reset() {
+ store.edit { it.clear() }
+ }
+}
+
+private fun Announcement.toKey() = when (this) {
+ Announcement.Space -> spaceAnnouncementKey
+ Announcement.NewNotificationSound -> newNotificationSoundKey
+}
diff --git a/features/announcement/impl/src/main/res/values-cs/translations.xml b/features/announcement/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 00000000000..cf7ead19622
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Zobrazit prostory, které jste vytvořili nebo ke kterým jste se připojili"
+ "Přijmout nebo odmítnout pozvánky do prostorů"
+ "Objevte všechny místnosti, do kterých můžete vstoupit ve svých prostorech"
+ "Připojit se k veřejným prostorům"
+ "Opustit všechny prostory, ke kterým jste se připojili"
+ "Filtrování, vytváření a správa prostorů bude brzy k dispozici."
+ "Vítejte v beta verzi prostorů! S touto první verzí můžete:"
+ "Představujeme prostory"
+
diff --git a/features/announcement/impl/src/main/res/values-da/translations.xml b/features/announcement/impl/src/main/res/values-da/translations.xml
new file mode 100644
index 00000000000..d07871b19e5
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Se grupper, du har oprettet eller tilmeldt dig"
+ "Acceptere eller afvise invitationer til grupper"
+ "Finde alle rum, du kan deltage i, i dine grupper"
+ "Deltage i offentlige grupper"
+ "Forlade de grupper, du har tilsluttet dig"
+ "Filtrering, oprettelse og administration af grupper kommer snart."
+ "Velkommen til betaversionen af Grupper! Med denne første version kan du:"
+ "Introduktion til Grupper"
+
diff --git a/features/announcement/impl/src/main/res/values-de/translations.xml b/features/announcement/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 00000000000..11f5f3a99cd
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Von dir erstellte oder beigetretene Spaces anzeigen"
+ "Einladungen zu Spaces annehmen oder ablehnen"
+ "Chats innerhalb deiner Spaces entdecken, um ihnen beizutreten"
+ "Öffentlichen Spaces beitreten"
+ "Spaces verlassen, bei denen du Mitglied bist"
+ "Das Filtern, Erstellen und Verwalten von Spaces ist bald verfügbar."
+ "Willkommen bei der Beta-Version von Spaces! Mit dieser ersten Version kannst du:"
+ "Einführung in Spaces"
+
diff --git a/features/announcement/impl/src/main/res/values-et/translations.xml b/features/announcement/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 00000000000..ee2ba9c3b41
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Vaadata kogukondi, mille oled loonud või millega oled liitunud"
+ "Nõustuda kutsetega liitumiseks kogukonnaga või sellest keelduda"
+ "Uurida neis kogukondades leiduvaid jututube ning nendega liituda"
+ "Liituda avalike kogukondadega"
+ "Lahkuda kogukonnast, millega oled liitunud"
+ "Kogukondade filtreerimine, loomine ja haldamine lisandub peagi"
+ "Tere tulemast kasutama kogukondade beetaversiooni! Selles esimeses versioonis saad sa:"
+ "Võtame kasutusele kogukonnad"
+
diff --git a/features/announcement/impl/src/main/res/values-fa/translations.xml b/features/announcement/impl/src/main/res/values-fa/translations.xml
new file mode 100644
index 00000000000..2e8902ae230
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-fa/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "دیدن فضاهایی که ساخته یا پیوستهاید"
+ "پذیرش یا رد دعوتها به فضاها"
+ "کشف تمامی اتاقهایی که میتوانید در فضاهایتان بپیوندید"
+ "پیوستن به فضاهای عمومی"
+ "ترک هر فضایی که پیوستهاید"
+ "پالایش، ایجاد و مدیریت کردن فضاها به زودی."
+ "به نگارش آزمایشی فضاها خوش آمدید! در این نگارش میتوانید:"
+ "معرّفی فضاها"
+
diff --git a/features/announcement/impl/src/main/res/values-fi/translations.xml b/features/announcement/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 00000000000..8e7674487f9
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Nähdä luomasi tai liittymäsi tilat"
+ "Hyväksyä tai hylätä kutsuja tiloihin"
+ "Löytää kaikki huoneet, joihin voit liittyä tiloissasi"
+ "Liittyä julkisiin tiloihin"
+ "Poistua mistä tahansa tilasta, johon olet liittynyt"
+ "Tilojen suodatus, luominen ja hallinta on tulossa pian."
+ "Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:"
+ "Esittelyssä tilat"
+
diff --git a/features/announcement/impl/src/main/res/values-fr/translations.xml b/features/announcement/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..7e042c65ff7
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Voir les espaces que vous avez créés ou rejoints"
+ "Accepter ou refuser les invitations aux espaces"
+ "Découvrir les salons que vous pouvez joindre depuis vos espaces"
+ "Rejoindre les espaces publics"
+ "Quitter les espaces dont vous êtes membre."
+ "Le filtrage, la création et la gestion des espaces seront bientôt disponibles."
+ "Bienvenue dans la version bêta des espaces! Avec cette première version, vous pourrez :"
+ "Ajout des espaces"
+
diff --git a/features/announcement/impl/src/main/res/values-hu/translations.xml b/features/announcement/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 00000000000..b09f70419b2
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Az Ön által létrehozott vagy csatlakozott térek megtekintése"
+ "A meghívások elfogadására vagy elutasítására a terekhez"
+ "Szobák felfedezése a terekben, amelyekhez csatlakozhat"
+ "Csatlakozás nyilvános terekhez"
+ "Terek elhagyása"
+ "Terek szűrése, készítése és kezelése hamarosan érkezik."
+ "Üdvözöljük a tér béta verziójában! Ezzel az első verzióval a következőket teheti:"
+ "Bemutatkoznak a terek"
+
diff --git a/features/announcement/impl/src/main/res/values-nb/translations.xml b/features/announcement/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 00000000000..553ff9f997d
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Se områder du har opprettet eller blitt med i"
+ "Godta eller avslå invitasjoner til områder"
+ "Oppdag alle rom du kan bli med i i dine områder"
+ "Bli med i offentlige områder"
+ "Forlat områder du har blitt med i"
+ "Oppretting, filtrering og administrasjon av områder kommer snart."
+ "Velkommen til betaversjonen av Områder! Med denne første versjonen kan du:"
+ "Vi introduserer Områder"
+
diff --git a/features/announcement/impl/src/main/res/values-pl/translations.xml b/features/announcement/impl/src/main/res/values-pl/translations.xml
new file mode 100644
index 00000000000..4308bdd81d4
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-pl/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Wyświetlić przestrzenie, które stworzyłeś lub do których dołączyłeś"
+ "Akceptować lub odrzucać zaproszenia"
+ "Odkrywać wszystkie pokoje, do których możesz dołączyć w swoich przestrzeniach"
+ "Dołączać do przestrzeni publicznych"
+ "Opuszczać jakąkolwiek przestrzeń, do której dołączyłeś"
+ "Filtrowanie, tworzenie i zarządzanie przestrzeniami pojawi się wkrótce."
+ "Witamy w wersji beta przestrzeni! W tej wersji możesz:"
+ "Przedstawiamy przestrzenie"
+
diff --git a/features/announcement/impl/src/main/res/values-ro/translations.xml b/features/announcement/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 00000000000..48fa06fca4e
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Vizualizați spațiile pe care le-ați creat sau la care v-ați alăturat"
+ "Acceptați sau refuzați invitațiile la spații"
+ "Descoperiți toate camerele la care vă puteți alătura în spațiile dumneavoastră."
+ "Alăturați-vă spațiilor publice"
+ "Părăsiți spațiile la care v-ați alăturat."
+ "Crearea și gestionarea spațiilor vor fi disponibile în curând."
+ "Bun venit la versiunea beta a Spațiilor! Cu această primă versiune puteți:"
+ "Vă prezentăm Spații"
+
diff --git a/features/announcement/impl/src/main/res/values-ru/translations.xml b/features/announcement/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 00000000000..7ff445bd69a
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Просмотр пространств, которые вы создали или к которым присоединились"
+ "Принимать или отклонять приглашения в пространства"
+ "Откройте для себя все комнаты, к которым вы можете присоединиться в своих пространствах."
+ "Присоединиться к публичному пространству"
+ "Покинуть все пространства, к которым вы присоединились"
+ "Работа с пространствами скоро станет доступна"
+ "Добро пожаловать в бета-версию Spaces! В этой первой версии вы сможете:"
+ "Представляем пространства"
+
diff --git a/features/announcement/impl/src/main/res/values-sk/translations.xml b/features/announcement/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 00000000000..0b305499a7f
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "Zobraziť priestory, ktoré ste vytvorili alebo ku ktorým ste sa pripojili"
+ "Prijímať alebo odmietať pozvánky do priestorov"
+ "Objaviť všetky miestnosti, do ktorých sa môžete pripojiť vo svojich priestoroch"
+ "Pripojiť sa k verejnému priestoru"
+ "Opustiť akékoľvek priestory, ku ktorým ste sa pridali"
+ "Filtrovanie, vytváranie a správa priestorov bude čoskoro k dispozícii."
+ "Vitajte v beta verzii priestorov! S touto prvou verziou môžete:"
+ "Predstavujeme priestory"
+
diff --git a/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml b/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 00000000000..a5b82752bc9
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,11 @@
+
+
+ "檢視您建立或加入的空間"
+ "接受或拒絕空間邀請"
+ "探索空間內您可以加入的任何聊天室"
+ "加入公開空間"
+ "離開任何您已加入的空間"
+ "篩選、建立與管理空間功能即將推出。"
+ "歡迎使用空間的測試版!此初始版本可讓您:"
+ "介紹空間"
+
diff --git a/features/announcement/impl/src/main/res/values-zh/translations.xml b/features/announcement/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 00000000000..66608eb6e52
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "查看您创建或加入的空间"
+ "接受或拒绝空间邀请"
+
diff --git a/features/announcement/impl/src/main/res/values/localazy.xml b/features/announcement/impl/src/main/res/values/localazy.xml
new file mode 100644
index 00000000000..5e7b8a6713e
--- /dev/null
+++ b/features/announcement/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,11 @@
+
+
+ "View spaces you\'ve created or joined"
+ "Accept or decline invites to spaces"
+ "Discover any rooms you can join in your spaces"
+ "Join public spaces"
+ "Leave any spaces you’ve joined"
+ "Filtering, creating and managing spaces is coming soon."
+ "Welcome to the beta version of Spaces! With this first version you can:"
+ "Introducing Spaces"
+
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt
new file mode 100644
index 00000000000..18deb8b2fd9
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class AnnouncementPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createAnnouncementPresenter()
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.showSpaceAnnouncement).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
+ val store = InMemoryAnnouncementStore()
+ val presenter = createAnnouncementPresenter(
+ announcementStore = store,
+ )
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.showSpaceAnnouncement).isFalse()
+ store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ val updatedState = awaitItem()
+ assertThat(updatedState.showSpaceAnnouncement).isTrue()
+ store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ val finalState = awaitItem()
+ assertThat(finalState.showSpaceAnnouncement).isFalse()
+ }
+ }
+}
+
+private fun createAnnouncementPresenter(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+) = AnnouncementPresenter(
+ announcementStore = announcementStore,
+)
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt
new file mode 100644
index 00000000000..e16619129c9
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
+import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultAnnouncementServiceTest {
+ @Test
+ fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ sut.showAnnouncement(Announcement.Space)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
+ // Simulate user close the announcement
+ sut.onAnnouncementDismissed(Announcement.Space)
+ // Entering again the space tab should not change the value
+ sut.showAnnouncement(Announcement.Space)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
+ }
+
+ @Test
+ fun `when showing NewNotificationSound announcement, announcement is set to show even if it was already shown`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ sut.showAnnouncement(Announcement.NewNotificationSound)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
+ // Simulate user close the announcement
+ sut.onAnnouncementDismissed(Announcement.NewNotificationSound)
+ // Calling again showAnnouncement should set it back to Show
+ sut.showAnnouncement(Announcement.NewNotificationSound)
+ assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
+ }
+
+ @Test
+ fun `test announcementsToShowFlow`() = runTest {
+ val announcementStore = InMemoryAnnouncementStore()
+ val sut = createDefaultAnnouncementService(
+ announcementStore = announcementStore,
+ )
+ sut.announcementsToShowFlow().test {
+ assertThat(awaitItem()).isEmpty()
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
+ assertThat(awaitItem()).containsExactly(Announcement.Space)
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
+ assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
+ announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
+ assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
+ announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
+ assertThat(awaitItem()).isEmpty()
+ }
+ }
+
+ private fun createDefaultAnnouncementService(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+ announcementPresenter: Presenter = Presenter { anAnnouncementState() },
+ spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() },
+ ) = DefaultAnnouncementService(
+ announcementStore = announcementStore,
+ announcementPresenter = announcementPresenter,
+ spaceAnnouncementPresenter = spaceAnnouncementPresenter,
+ )
+}
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt
new file mode 100644
index 00000000000..672f6774076
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.impl.store.AnnouncementStatus
+import io.element.android.features.announcement.impl.store.AnnouncementStore
+import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class SpaceAnnouncementPresenterTest {
+ @Test
+ fun `present - when user continues, the store is updated`() = runTest {
+ val store = InMemoryAnnouncementStore()
+ val presenter = createSpaceAnnouncementPresenter(
+ announcementStore = store,
+ )
+ presenter.test {
+ assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
+ val state = awaitItem()
+ state.eventSink(SpaceAnnouncementEvents.Continue)
+ assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
+ }
+ }
+}
+
+private fun createSpaceAnnouncementPresenter(
+ announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
+) = SpaceAnnouncementPresenter(
+ announcementStore = announcementStore,
+)
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt
new file mode 100644
index 00000000000..ad3d83f1b50
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.spaces
+
+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.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpaceAnnouncementViewTest {
+ @get:Rule val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on back sends a SpaceAnnouncementEvents`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setSpaceAnnouncementView(
+ aSpaceAnnouncementState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
+ }
+
+ @Test
+ fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setSpaceAnnouncementView(
+ aSpaceAnnouncementState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_continue)
+ eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
+ }
+}
+
+private fun AndroidComposeTestRule.setSpaceAnnouncementView(
+ state: SpaceAnnouncementState,
+) {
+ setContent {
+ SpaceAnnouncementView(
+ state = state,
+ )
+ }
+}
diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt
new file mode 100644
index 00000000000..ab3e85124f2
--- /dev/null
+++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.announcement.impl.store
+
+import io.element.android.features.announcement.api.Announcement
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class InMemoryAnnouncementStore(
+ initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
+ initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
+) : AnnouncementStore {
+ private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
+ private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
+
+ override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
+ announcement.toMutableStateFlow().value = status
+ }
+
+ override fun announcementStatusFlow(announcement: Announcement): Flow {
+ return announcement.toMutableStateFlow().asStateFlow()
+ }
+
+ override suspend fun reset() {
+ spaceAnnouncement.value = AnnouncementStatus.NeverShown
+ newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
+ }
+
+ private fun Announcement.toMutableStateFlow() = when (this) {
+ Announcement.Space -> spaceAnnouncement
+ Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
+ }
+}
diff --git a/features/announcement/test/build.gradle.kts b/features/announcement/test/build.gradle.kts
new file mode 100644
index 00000000000..d9e9251798e
--- /dev/null
+++ b/features/announcement/test/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.announcement.test"
+}
+
+dependencies {
+ implementation(projects.features.announcement.api)
+ implementation(libs.coroutines.core)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt
new file mode 100644
index 00000000000..fefb61d722f
--- /dev/null
+++ b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.rageshake.test.logs
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
+import io.element.android.tests.testutils.lambda.lambdaError
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeAnnouncementService(
+ initialAnnouncementsToShowFlowValue: List = emptyList(),
+ val showAnnouncementResult: (Announcement) -> Unit = { lambdaError() },
+ val onAnnouncementDismissedResult: (Announcement) -> Unit = { lambdaError() },
+ val renderResult: (Modifier) -> Unit = { lambdaError() },
+) : AnnouncementService {
+ private val announcementsToShowFlowValue = MutableStateFlow(initialAnnouncementsToShowFlowValue)
+
+ override suspend fun showAnnouncement(announcement: Announcement) {
+ showAnnouncementResult(announcement)
+ }
+
+ override suspend fun onAnnouncementDismissed(announcement: Announcement) {
+ onAnnouncementDismissedResult(announcement)
+ }
+
+ override fun announcementsToShowFlow(): Flow> {
+ return announcementsToShowFlowValue.asStateFlow()
+ }
+
+ fun emitAnnouncementsToShow(value: List) {
+ announcementsToShowFlowValue.value = value
+ }
+
+ @Composable
+ override fun Render(modifier: Modifier) {
+ renderResult(modifier)
+ }
+}
diff --git a/features/cachecleaner/api/build.gradle.kts b/features/cachecleaner/api/build.gradle.kts
index 7bfe8f6c5d2..705107298a1 100644
--- a/features/cachecleaner/api/build.gradle.kts
+++ b/features/cachecleaner/api/build.gradle.kts
@@ -1,9 +1,8 @@
-import extension.setupAnvil
-
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,8 +14,6 @@ android {
namespace = "io.element.android.features.cachecleaner.api"
}
-setupAnvil()
-
dependencies {
implementation(projects.libraries.architecture)
implementation(libs.androidx.startup)
diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt
index 192886ef17d..b5a5396b16c 100644
--- a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt
+++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt
deleted file mode 100644
index 9c44c1d5aea..00000000000
--- a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.cachecleaner.api
-
-import com.squareup.anvil.annotations.ContributesTo
-import io.element.android.libraries.di.AppScope
-
-@ContributesTo(AppScope::class)
-interface CacheCleanerBindings {
- fun cacheCleaner(): CacheCleaner
-}
diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt
deleted file mode 100644
index 4ac5e0b8145..00000000000
--- a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2022-2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.cachecleaner.api
-
-import android.content.Context
-import androidx.startup.Initializer
-import io.element.android.libraries.architecture.bindings
-
-class CacheCleanerInitializer : Initializer {
- override fun create(context: Context) {
- context.bindings().cacheCleaner().clearCache()
- }
-
- override fun dependencies(): List>> = emptyList()
-}
diff --git a/features/cachecleaner/impl/build.gradle.kts b/features/cachecleaner/impl/build.gradle.kts
index 137f00e31e9..3321e2a80f6 100644
--- a/features/cachecleaner/impl/build.gradle.kts
+++ b/features/cachecleaner/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,15 +17,12 @@ android {
namespace = "io.element.android.features.cachecleaner.impl"
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.cachecleaner.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.test.truth)
- testImplementation(projects.tests.testutils)
+ testCommonDependencies(libs)
}
diff --git a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/CacheCleanerBindings.kt b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/CacheCleanerBindings.kt
new file mode 100644
index 00000000000..2137b7b9058
--- /dev/null
+++ b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/CacheCleanerBindings.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.cachecleaner.impl
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesTo
+import io.element.android.features.cachecleaner.api.CacheCleaner
+
+@ContributesTo(AppScope::class)
+interface CacheCleanerBindings {
+ fun cacheCleaner(): CacheCleaner
+}
diff --git a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
index 86e6432cc5d..44351412f07 100644
--- a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
+++ b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
@@ -1,30 +1,30 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.cachecleaner.impl
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.cachecleaner.api.CacheCleaner
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
-import javax.inject.Inject
/**
* Default implementation of [CacheCleaner].
*/
@ContributesBinding(AppScope::class)
-class DefaultCacheCleaner @Inject constructor(
+class DefaultCacheCleaner(
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
diff --git a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
index 997c0fe7039..6094f1e16d6 100644
--- a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
+++ b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/api/build.gradle.kts b/features/call/api/build.gradle.kts
index 34cb8959388..1480a31fd3d 100644
--- a/features/call/api/build.gradle.kts
+++ b/features/call/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,7 +16,6 @@ android {
}
dependencies {
- implementation(projects.anvilannotations)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
index 2c279b77258..5beb9f7c54a 100644
--- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt
index 42fd4e873d3..a6932a148e3 100644
--- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt
index f7abf845c54..25720596bac 100644
--- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt
index 53e1b8c8461..caa557f4dec 100644
--- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -29,6 +30,7 @@ interface ElementCallEntryPoint {
* @param senderName The name of the sender of the event that started the call.
* @param avatarUrl The avatar url of the room or DM.
* @param timestamp The timestamp of the event that started the call.
+ * @param expirationTimestamp The timestamp at which the call should stop ringing.
* @param notificationChannelId The id of the notification channel to use for the call notification.
* @param textContent The text content of the notification. If null the default content from the system will be used.
*/
@@ -40,6 +42,7 @@ interface ElementCallEntryPoint {
senderName: String?,
avatarUrl: String?,
timestamp: Long,
+ expirationTimestamp: Long,
notificationChannelId: String,
textContent: String?,
)
diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts
index 13922366c2c..e77c09e19a8 100644
--- a/features/call/impl/build.gradle.kts
+++ b/features/call/impl/build.gradle.kts
@@ -1,11 +1,13 @@
import extension.buildConfigFieldStr
import extension.readLocalProperty
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -60,7 +62,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
@@ -72,7 +74,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.impl)
- implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
@@ -87,20 +89,15 @@ dependencies {
implementation(libs.element.call.embedded)
api(projects.features.call.api)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.test.mockk)
+ testCommonDependencies(libs, true)
testImplementation(projects.features.call.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
+ testImplementation(projects.services.appnavstate.impl)
testImplementation(projects.services.appnavstate.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+ testImplementation(projects.services.toolbox.test)
}
diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml
index dfc3c13632c..daf1a910c94 100644
--- a/features/call/impl/src/main/AndroidManifest.xml
+++ b/features/call/impl/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt
index 009840743c5..9ed479f5b8a 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt
@@ -1,27 +1,27 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl
import android.content.Context
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.features.call.impl.utils.IntentProvider
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultElementCallEntryPoint @Inject constructor(
+class DefaultElementCallEntryPoint(
@ApplicationContext private val context: Context,
private val activeCallManager: ActiveCallManager,
) : ElementCallEntryPoint {
@@ -42,6 +42,7 @@ class DefaultElementCallEntryPoint @Inject constructor(
senderName: String?,
avatarUrl: String?,
timestamp: Long,
+ expirationTimestamp: Long,
notificationChannelId: String,
textContent: String?,
) {
@@ -54,6 +55,7 @@ class DefaultElementCallEntryPoint @Inject constructor(
senderName = senderName,
avatarUrl = avatarUrl,
timestamp = timestamp,
+ expirationTimestamp = expirationTimestamp,
notificationChannelId = notificationChannelId,
textContent = textContent,
)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
index c18fef410ee..421ac9d37a3 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -41,5 +42,8 @@ data class WidgetMessage(
@SerialName("send_event")
SendEvent,
+
+ @SerialName("content_loaded")
+ ContentLoaded,
}
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
index e92b4c64fae..66efe82d380 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
@@ -1,17 +1,18 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.di
-import com.squareup.anvil.annotations.ContributesTo
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver
import io.element.android.features.call.impl.ui.ElementCallActivity
import io.element.android.features.call.impl.ui.IncomingCallActivity
-import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface CallBindings {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt
index 7c97cc85295..dcc434e84d9 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -26,4 +27,6 @@ data class CallNotificationData(
val notificationChannelId: String,
val timestamp: Long,
val textContent: String?,
+ // Expiration timestamp in millis since epoch
+ val expirationTimestamp: Long,
) : Parcelable
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
index de31b0b02bb..e7d270ef8e1 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.notifications
@@ -15,13 +16,16 @@ import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.Person
+import dev.zacsweers.metro.Inject
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver
import io.element.android.features.call.impl.ui.IncomingCallActivity
import io.element.android.features.call.impl.utils.IntentProvider
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.CommonDrawables
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -29,13 +33,13 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
-import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/**
* Creates a notification for a ringing call.
*/
-class RingingCallNotificationCreator @Inject constructor(
+@Inject
+class RingingCallNotificationCreator(
@ApplicationContext private val context: Context,
private val matrixClientProvider: MatrixClientProvider,
private val imageLoaderHolder: ImageLoaderHolder,
@@ -63,15 +67,24 @@ class RingingCallNotificationCreator @Inject constructor(
roomAvatarUrl: String?,
notificationChannelId: String,
timestamp: Long,
+ expirationTimestamp: Long,
textContent: String?,
): Notification? {
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val imageLoader = imageLoaderHolder.get(matrixClient)
- val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader)
+ val userIcon = notificationBitmapLoader.getUserIcon(
+ avatarData = AvatarData(
+ id = roomId.value,
+ name = roomName,
+ url = roomAvatarUrl,
+ size = AvatarSize.RoomDetailsHeader,
+ ),
+ imageLoader = imageLoader,
+ )
val caller = Person.Builder()
.setName(senderDisplayName)
- .setIcon(largeIcon)
+ .setIcon(userIcon)
.setImportant(true)
.build()
@@ -87,6 +100,7 @@ class RingingCallNotificationCreator @Inject constructor(
notificationChannelId = notificationChannelId,
timestamp = timestamp,
textContent = textContent,
+ expirationTimestamp = expirationTimestamp,
)
val declineIntent = PendingIntentCompat.getBroadcast(
@@ -119,12 +133,8 @@ class RingingCallNotificationCreator @Inject constructor(
.setWhen(timestamp)
.setOngoing(true)
.setShowWhen(false)
- .apply {
- if (textContent != null) {
- setContentText(textContent)
- // Else the content text is set by the style (will be "Incoming call")
- }
- }
+ // If textContent is null, the content text is set by the style (will be "Incoming call")
+ .setContentText(textContent)
.setSound(Settings.System.DEFAULT_RINGTONE_URI, AudioManager.STREAM_RING)
.setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds)
.setContentIntent(answerIntent)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt
index 79b075cf923..9522d44b220 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
index b64852d283c..5125b464dc1 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,16 +14,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.call.impl.utils.PipController
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.log.logger.LoggerTag
import kotlinx.coroutines.launch
import timber.log.Timber
-import javax.inject.Inject
private val loggerTag = LoggerTag("PiP")
-class PictureInPicturePresenter @Inject constructor(
+@Inject
+class PictureInPicturePresenter(
pipSupportProvider: PipSupportProvider,
) : Presenter {
private val isPipSupported = pipSupportProvider.isPipSupported()
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt
index 1bd84b409ab..b1fef4f28b1 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt
index f2afcd01962..6324820eecd 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt
index 4d5441367ee..54109f01802 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,11 +12,10 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import javax.inject.Inject
+import io.element.android.libraries.di.annotations.ApplicationContext
interface PipSupportProvider {
@ChecksSdkIntAtLeast(Build.VERSION_CODES.O)
@@ -23,7 +23,7 @@ interface PipSupportProvider {
}
@ContributesBinding(AppScope::class)
-class DefaultPipSupportProvider @Inject constructor(
+class DefaultPipSupportProvider(
@ApplicationContext private val context: Context,
) : PipSupportProvider {
override fun isPipSupported(): Boolean {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
index 593d37a1f2c..90f74a37fd1 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
index c857c9e2c86..d2cbb0184d5 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,6 +12,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.IntentCompat
+import dev.zacsweers.metro.Inject
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.notifications.CallNotificationData
@@ -19,7 +21,6 @@ import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
/**
* Broadcast receiver to decline the incoming call.
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
index b779465a342..89c9fdb52ca 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,7 +22,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
-import androidx.core.graphics.drawable.IconCompat
import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.ui.ElementCallActivity
import io.element.android.libraries.core.extensions.runCatchingExceptions
@@ -68,7 +68,7 @@ class CallForegroundService : Service() {
val callActivityIntent = Intent(this, ElementCallActivity::class.java)
val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false)
val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id)
- .setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification))
+ .setSmallIcon(CommonDrawables.ic_notification)
.setContentTitle(getString(R.string.call_foreground_service_title_android))
.setContentText(getString(R.string.call_foreground_service_message_android))
.setContentIntent(pendingIntent)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
index 45bfa81e388..8fbbce896f7 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
index 04999c82ab6..7fbcf1ba71a 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -17,9 +18,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.call.api.CallType
@@ -33,14 +34,11 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.annotations.AppCoroutineScope
-import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
-import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
-import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@@ -52,7 +50,8 @@ import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
-class CallScreenPresenter @AssistedInject constructor(
+@AssistedInject
+class CallScreenPresenter(
@Assisted private val callType: CallType,
@Assisted private val navigator: CallScreenNavigator,
private val callWidgetProvider: CallWidgetProvider,
@@ -64,9 +63,9 @@ class CallScreenPresenter @AssistedInject constructor(
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
- private val activeRoomsHolder: ActiveRoomsHolder,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
+ private val widgetMessageSerializer: WidgetMessageSerializer,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -75,7 +74,6 @@ class CallScreenPresenter @AssistedInject constructor(
private val isInWidgetMode = callType is CallType.RoomCall
private val userAgent = userAgentProvider.provide()
- private var notifiedCallStart = false
@Composable
override fun present(): CallScreenState {
@@ -83,7 +81,7 @@ class CallScreenPresenter @AssistedInject constructor(
val urlState = remember { mutableStateOf>(AsyncData.Uninitialized) }
val callWidgetDriver = remember { mutableStateOf(null) }
val messageInterceptor = remember { mutableStateOf(null) }
- var isJoinedCall by rememberSaveable { mutableStateOf(false) }
+ var isWidgetLoaded by rememberSaveable { mutableStateOf(false) }
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
var webViewError by remember { mutableStateOf(null) }
val languageTag = languageTagProvider.provideLanguageTag()
@@ -143,8 +141,8 @@ class CallScreenPresenter @AssistedInject constructor(
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
if (parsedMessage.action == WidgetMessage.Action.Close) {
close(callWidgetDriver.value, navigator)
- } else if (parsedMessage.action == WidgetMessage.Action.Join) {
- isJoinedCall = true
+ } else if (parsedMessage.action == WidgetMessage.Action.ContentLoaded) {
+ isWidgetLoaded = true
}
}
}
@@ -155,8 +153,8 @@ class CallScreenPresenter @AssistedInject constructor(
// Wait for the call to be joined, if it takes too long, we display an error
delay(10.seconds)
- if (!isJoinedCall) {
- Timber.w("The call took too long to be joined. Displaying an error before exiting.")
+ if (!isWidgetLoaded) {
+ Timber.w("The call took too long to load. Displaying an error before exiting.")
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
webViewError = ""
@@ -164,15 +162,15 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
- fun handleEvents(event: CallScreenEvents) {
+ fun handleEvent(event: CallScreenEvents) {
when (event) {
is CallScreenEvents.Hangup -> {
val widgetId = callWidgetDriver.value?.id
val interceptor = messageInterceptor.value
- if (widgetId != null && interceptor != null && isJoinedCall) {
+ if (widgetId != null && interceptor != null && isWidgetLoaded) {
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
sendHangupMessage(widgetId, interceptor)
- isJoinedCall = false
+ isWidgetLoaded = false
coroutineScope.launch {
// Wait for a couple of seconds to receive the hangup message
@@ -202,9 +200,9 @@ class CallScreenPresenter @AssistedInject constructor(
urlState = urlState.value,
webViewError = webViewError,
userAgent = userAgent,
- isCallActive = isJoinedCall,
+ isCallActive = isWidgetLoaded,
isInWidgetMode = isInWidgetMode,
- eventSink = { handleEvents(it) },
+ eventSink = ::handleEvent,
)
}
@@ -246,11 +244,9 @@ class CallScreenPresenter @AssistedInject constructor(
}
coroutineScope.launch {
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
- client.syncService().syncState
+ client.syncService.syncState
.collect { state ->
- if (state == SyncState.Running) {
- client.notifyCallStartIfNeeded(callType.roomId)
- } else {
+ if (state != SyncState.Running) {
appForegroundStateService.updateIsInCallState(true)
}
}
@@ -263,34 +259,8 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
- private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
- if (notifiedCallStart) return
-
- val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
- val sendCallNotificationResult = if (activeRoomForSession != null) {
- Timber.d("Notifying call start for room $roomId. Has room call: ${activeRoomForSession.info().hasRoomCall}")
- activeRoomForSession.sendCallNotificationIfNeeded()
- } else {
- // Instantiate the room from the session and roomId and send the notification
- getJoinedRoom(roomId)?.use { room ->
- Timber.d("Notifying call start for room $roomId. Has room call: ${room.info().hasRoomCall}")
- room.sendCallNotificationIfNeeded()
- } ?: run {
- Timber.w("No room found for session $sessionId and room $roomId, skipping call notification.")
- return
- }
- }
-
- sendCallNotificationResult.fold(
- onSuccess = { notifiedCallStart = true },
- onFailure = { error ->
- Timber.e(error, "Failed to send call notification for room $roomId.")
- }
- )
- }
-
private fun parseMessage(message: String): WidgetMessage? {
- return WidgetMessageSerializer.deserialize(message).getOrNull()
+ return widgetMessageSerializer.deserialize(message).getOrNull()
}
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
@@ -301,7 +271,7 @@ class CallScreenPresenter @AssistedInject constructor(
action = WidgetMessage.Action.HangUp,
data = null,
)
- messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message))
+ messageInterceptor.sendMessage(widgetMessageSerializer.serialize(message))
}
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
index 5b1288db429..c07594aebb7 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
index 77680a8e003..3e72f96f878 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
index 88d2e81f8bb..f8657a9ece7 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
@@ -1,14 +1,14 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint
-import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
@@ -60,6 +60,7 @@ interface CallScreenNavigator {
internal fun CallScreenView(
state: CallScreenState,
pipState: PictureInPictureState,
+ onConsoleMessage: (ConsoleMessage) -> Unit,
requestPermissions: (Array, RequestPermissionCallback) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -108,6 +109,7 @@ internal fun CallScreenView(
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
+ onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
@@ -174,6 +176,7 @@ private fun CallWebView(
url: AsyncData,
userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit,
+ onConsoleMessage: (ConsoleMessage) -> Unit,
onCreateWebView: (WebView) -> Unit,
onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier,
@@ -188,7 +191,11 @@ private fun CallWebView(
factory = { context ->
WebView(context).apply {
onCreateWebView(this)
- setup(userAgent, onPermissionsRequest)
+ setup(
+ userAgent = userAgent,
+ onPermissionsRequested = onPermissionsRequest,
+ onConsoleMessage = onConsoleMessage,
+ )
}
},
update = { webView ->
@@ -208,6 +215,7 @@ private fun CallWebView(
private fun WebView.setup(
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
+ onConsoleMessage: (ConsoleMessage) -> Unit,
) {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -232,35 +240,7 @@ private fun WebView.setup(
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
- val priority = when (consoleMessage.messageLevel()) {
- ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
- ConsoleMessage.MessageLevel.WARNING -> Log.WARN
- else -> Log.DEBUG
- }
-
- val message = buildString {
- append(consoleMessage.sourceId())
- append(":")
- append(consoleMessage.lineNumber())
- append(" ")
- append(consoleMessage.message())
- }
-
- if (message.contains("password=")) {
- // Avoid logging any messages that contain "password" to prevent leaking sensitive information
- return true
- }
-
- Timber.tag("WebView").log(
- priority = priority,
- message = buildString {
- append(consoleMessage.sourceId())
- append(":")
- append(consoleMessage.lineNumber())
- append(" ")
- append(consoleMessage.message())
- },
- )
+ onConsoleMessage(consoleMessage)
return true
}
}
@@ -286,6 +266,7 @@ internal fun CallScreenViewPreview(
state = state,
pipState = aPictureInPictureState(),
requestPermissions = { _, _ -> },
+ onConsoleMessage = {},
)
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt
new file mode 100644
index 00000000000..0c18c3e1a49
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.call.impl.ui
+
+import io.element.android.features.call.api.CallType
+import io.element.android.libraries.matrix.api.core.SessionId
+
+fun CallType.getSessionId(): SessionId? {
+ return when (this) {
+ is CallType.ExternalUrl -> null
+ is CallType.RoomCall -> sessionId
+ }
+}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
index 870c2244648..bf4f8362945 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -23,13 +24,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.rememberUpdatedState
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.content.IntentCompat
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
+import dev.zacsweers.metro.Inject
+import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.CallType.ExternalUrl
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
@@ -41,6 +46,7 @@ import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
+import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus
@@ -50,7 +56,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import timber.log.Timber
-import javax.inject.Inject
private val loggerTag = LoggerTag("ElementCallActivity")
@@ -65,6 +70,7 @@ class ElementCallActivity :
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var audioFocus: AudioFocus
+ @Inject lateinit var consoleMessageLogger: ConsoleMessageLogger
private lateinit var presenter: Presenter
@@ -79,7 +85,7 @@ class ElementCallActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- applicationContext.bindings().inject(this)
+ bindings().inject(this)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -103,9 +109,13 @@ class ElementCallActivity :
setContent {
val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
+ val colors by remember(webViewTarget.value?.getSessionId()) {
+ enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId())
+ }.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
- enterpriseService = enterpriseService,
+ compoundLight = colors.light,
+ compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val state = presenter.present()
@@ -119,6 +129,9 @@ class ElementCallActivity :
CallScreenView(
state = state,
pipState = pipState,
+ onConsoleMessage = {
+ consoleMessageLogger.log("ElementCall", it)
+ },
requestPermissions = { permissions, callback ->
requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
index dc77af9b774..714360a702f 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,8 +12,13 @@ import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
+import dev.zacsweers.metro.Inject
+import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings
@@ -30,7 +36,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import javax.inject.Inject
/**
* Activity that's displayed as a full screen intent when an incoming call is received.
@@ -64,7 +69,7 @@ class IncomingCallActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- applicationContext.bindings().inject(this)
+ bindings().inject(this)
// Set flags so it can be displayed in the lock screen
@Suppress("DEPRECATION")
@@ -78,9 +83,13 @@ class IncomingCallActivity : AppCompatActivity() {
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
if (notificationData != null) {
setContent {
+ val colors by remember {
+ enterpriseService.semanticColorsFlow(sessionId = notificationData.sessionId)
+ }.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
- enterpriseService = enterpriseService,
+ compoundLight = colors.light,
+ compoundDark = colors.dark,
buildMeta = buildMeta,
) {
IncomingCallScreen(
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
index 954fa3568f3..682c4cec739 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -176,6 +177,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview {
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
+ expirationTimestamp = 1000L,
),
onAnswer = {},
onCancel = {},
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt
index cce855139aa..c9abd7b7358 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/LanguageTagProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,9 +10,8 @@ package io.element.android.features.call.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
interface LanguageTagProvider {
@Composable
@@ -19,7 +19,7 @@ interface LanguageTagProvider {
}
@ContributesBinding(AppScope::class)
-class DefaultLanguageTagProvider @Inject constructor() : LanguageTagProvider {
+class DefaultLanguageTagProvider : LanguageTagProvider {
@Composable
override fun provideLanguageTag(): String? {
return LocalConfiguration.current.locales.get(0)?.toLanguageTag()
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
index d4f5abb59db..91fcc2593e2 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,17 +16,17 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.libraries.core.extensions.runCatchingExceptions
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
+import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
@@ -33,6 +34,7 @@ import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
import io.element.android.services.appnavstate.api.AppForegroundStateService
+import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -52,8 +54,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
-import javax.inject.Inject
-import kotlin.time.Duration.Companion.seconds
+import kotlin.math.min
/**
* Manages the active call state.
@@ -86,7 +87,7 @@ interface ActiveCallManager {
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class DefaultActiveCallManager @Inject constructor(
+class DefaultActiveCallManager(
@ApplicationContext context: Context,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
@@ -97,8 +98,9 @@ class DefaultActiveCallManager @Inject constructor(
private val defaultCurrentCallService: DefaultCurrentCallService,
private val appForegroundStateService: AppForegroundStateService,
private val imageLoaderHolder: ImageLoaderHolder,
+ private val systemClock: SystemClock,
) : ActiveCallManager {
- private val tag = "DefaultActiveCallManager"
+ private val tag = "ActiveCallManager"
private var timedOutCallJob: Job? = null
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -117,8 +119,20 @@ class DefaultActiveCallManager @Inject constructor(
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
mutex.withLock {
+ val ringDuration =
+ min(
+ notificationData.expirationTimestamp - systemClock.epochMillis(),
+ ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L
+ )
+
+ if (ringDuration < 0) {
+ // Should already have stopped ringing, ignore.
+ Timber.tag(tag).d("Received timed-out incoming ringing call for room id: ${notificationData.roomId}, cancel ringing")
+ return
+ }
+
appForegroundStateService.updateHasRingingCall(true)
- Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}")
+ Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}, ringDuration(ms): $ringDuration")
if (activeCall.value != null) {
displayMissedCallNotification(notificationData)
Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData")
@@ -137,14 +151,14 @@ class DefaultActiveCallManager @Inject constructor(
showIncomingCallNotification(notificationData)
// Wait for the ringing call to time out
- delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
+ delay(timeMillis = ringDuration)
incomingCallTimedOut(displayMissedCallNotification = true)
}
// Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data
if (activeWakeLock?.isHeld == false) {
Timber.tag(tag).d("Acquiring partial wakelock")
- activeWakeLock.acquire(ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L)
+ activeWakeLock.acquire(ringDuration)
}
}
}
@@ -179,12 +193,22 @@ class DefaultActiveCallManager @Inject constructor(
}
override suspend fun hungUpCall(callType: CallType) = mutex.withLock {
- if (activeCall.value?.callType != callType) {
+ Timber.tag(tag).d("Hung up call: $callType")
+ val currentActiveCall = activeCall.value ?: run {
+ Timber.tag(tag).w("No active call, ignoring hang up")
+ return
+ }
+ if (currentActiveCall.callType != callType) {
Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring")
return
}
-
- Timber.tag(tag).d("Hung up call: $callType")
+ if (currentActiveCall.callState is CallState.Ringing) {
+ // Decline the call
+ val notificationData = currentActiveCall.callState.notificationData
+ matrixClientProvider.getOrRestore(notificationData.sessionId).getOrNull()
+ ?.getRoom(notificationData.roomId)
+ ?.declineCall(notificationData.eventId)
+ }
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
@@ -225,6 +249,7 @@ class DefaultActiveCallManager @Inject constructor(
notificationChannelId = notificationData.notificationChannelId,
timestamp = notificationData.timestamp,
textContent = notificationData.textContent,
+ expirationTimestamp = notificationData.expirationTimestamp,
) ?: return
runCatchingExceptions {
notificationManagerCompat.notify(
@@ -255,6 +280,43 @@ class DefaultActiveCallManager @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() {
+ activeCall
+ .filterNotNull()
+ .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
+ .flatMapLatest { activeCall ->
+ val callType = activeCall.callType as CallType.RoomCall
+ val ringingInfo = activeCall.callState as CallState.Ringing
+ val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
+ Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
+ return@flatMapLatest flowOf()
+ }
+ val room = client.getRoom(callType.roomId) ?: run {
+ Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
+ return@flatMapLatest flowOf()
+ }
+
+ Timber.tag(tag).d("Found room for ringing call: ${room.roomId}")
+
+ // If we have declined from another phone we want to stop ringing.
+ room.subscribeToCallDecline(ringingInfo.notificationData.eventId)
+ .filter { decliner ->
+ Timber.tag(tag).d("Call: $activeCall was declined by $decliner")
+ // only want to listen if the call was declined from another of my sessions,
+ // (we are ringing for an incoming call in a DM)
+ decliner == client.sessionId
+ }
+ }
+ .onEach { decliner ->
+ Timber.tag(tag).d("Call: $activeCall was declined by user from another session")
+ // Remove the active call and cancel the notification
+ activeCall.value = null
+ if (activeWakeLock?.isHeld == true) {
+ Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
+ activeWakeLock.release()
+ }
+ cancelIncomingCallNotification()
+ }
+ .launchIn(coroutineScope)
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user
// has joined the call from another session.
activeCall
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
index c81b87746e6..f5433c15a0d 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,9 +10,10 @@ package io.element.android.features.call.impl.utils
import android.net.Uri
import androidx.core.net.toUri
-import javax.inject.Inject
+import dev.zacsweers.metro.Inject
-class CallIntentDataParser @Inject constructor() {
+@Inject
+class CallIntentDataParser {
private val validHttpSchemes = sequenceOf("https")
private val knownHosts = sequenceOf(
"call.element.io",
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
index ec4c7a79d48..6ce73bc5e6f 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt
index c978aba7e8b..6ba075b7fc5 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt
@@ -1,20 +1,20 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.utils
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.call.impl.BuildConfig
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultCallAnalyticCredentialsProvider @Inject constructor() : CallAnalyticCredentialsProvider {
+class DefaultCallAnalyticCredentialsProvider : CallAnalyticCredentialsProvider {
override val posthogUserId: String? = BuildConfig.POSTHOG_USER_ID.takeIf { it.isNotBlank() }
override val posthogApiHost: String? = BuildConfig.POSTHOG_API_HOST.takeIf { it.isNotBlank() }
override val posthogApiKey: String? = BuildConfig.POSTHOG_API_KEY.takeIf { it.isNotBlank() }
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
index d87826fe460..1728a0cacee 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
@@ -1,28 +1,29 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.utils
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.extensions.runCatchingExceptions
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.flow.firstOrNull
-import javax.inject.Inject
private const val EMBEDDED_CALL_WIDGET_BASE_URL = "https://appassets.androidplatform.net/element-call/index.html"
@ContributesBinding(AppScope::class)
-class DefaultCallWidgetProvider @Inject constructor(
+class DefaultCallWidgetProvider(
private val matrixClientsProvider: MatrixClientProvider,
private val appPreferencesStore: AppPreferencesStore,
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
@@ -43,8 +44,14 @@ class DefaultCallWidgetProvider @Inject constructor(
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
- val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
- val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted)
+ val roomInfo = room.info()
+ val isEncrypted = roomInfo.isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
+ val widgetSettings = callWidgetSettingsProvider.provide(
+ baseUrl = baseUrl,
+ encrypted = isEncrypted,
+ direct = room.isDm(),
+ hasActiveCall = roomInfo.hasRoomCall,
+ )
val callUrl = room.generateWidgetWebViewUrl(
widgetSettings = widgetSettings,
clientId = clientId,
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt
index 0e561713b3b..1c8c0b342ba 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt
@@ -1,23 +1,23 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.utils
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.api.CurrentCallService
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
-import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class DefaultCurrentCallService @Inject constructor() : CurrentCallService {
+class DefaultCurrentCallService : CurrentCallService {
override val currentCall = MutableStateFlow(CurrentCall.None)
fun onCallStarted(call: CurrentCall) {
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt
index 774367a52d2..0f74ba86d4c 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
index f1e3e2756bd..b259816f9d5 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
index b7d1fdf466f..d4811d65d5a 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,13 +22,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
/**
* This class manages the audio devices for a WebView.
@@ -42,6 +45,13 @@ class WebViewAudioManager(
private val coroutineScope: CoroutineScope,
private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit,
) {
+ private val json by lazy {
+ Json {
+ encodeDefaults = true
+ explicitNulls = false
+ }
+ }
+
/**
* Whether to disable bluetooth audio devices. This must be done on Android versions lower than Android 12,
* since the WebView approach breaks when using the legacy Bluetooth audio APIs.
@@ -84,6 +94,11 @@ class WebViewAudioManager(
?.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "${webView.context.packageName}:ProximitySensorCallWakeLock")
}
+ /**
+ * Used to ensure that only one coroutine can access the proximity sensor wake lock at a time, preventing re-acquiring or re-releasing it.
+ */
+ private val proximitySensorMutex = Mutex()
+
/**
* This listener tracks the current communication device and updates the WebView when it changes.
*/
@@ -208,8 +223,12 @@ class WebViewAudioManager(
return
}
- if (proximitySensorWakeLock?.isHeld == true) {
- proximitySensorWakeLock?.release()
+ coroutineScope.launch {
+ proximitySensorMutex.withLock {
+ if (proximitySensorWakeLock?.isHeld == true) {
+ proximitySensorWakeLock?.release()
+ }
+ }
}
audioManager.mode = AudioManager.MODE_NORMAL
@@ -235,7 +254,6 @@ class WebViewAudioManager(
private fun registerWebViewDeviceSelectedCallback() {
val webViewAudioDeviceSelectedCallback = AndroidWebViewAudioBridge(
onAudioDeviceSelected = { selectedDeviceId ->
- Timber.d("Audio device selected in webview, id: $selectedDeviceId")
previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId }
audioManager.selectAudioDevice(selectedDeviceId)
},
@@ -243,7 +261,7 @@ class WebViewAudioManager(
coroutineScope.launch(Dispatchers.Main) {
// Even with the callback, it seems like starting the audio takes a bit on the webview side,
// so we add an extra delay here to make sure it's ready
- delay(500.milliseconds)
+ delay(2.seconds)
// Calling this ahead of time makes the default audio device to not use the right audio stream
setAvailableAudioDevices()
@@ -298,11 +316,7 @@ class WebViewAudioManager(
devices: List = listAudioDevices().map(SerializableAudioDevice::fromAudioDeviceInfo),
) {
Timber.d("Updating available audio devices")
- val jsonSerializer = Json {
- encodeDefaults = true
- explicitNulls = false
- }
- val deviceList = jsonSerializer.encodeToString(devices)
+ val deviceList = json.encodeToString(devices)
webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", {
Timber.d("Audio: setAvailableOutputDevices result: $it")
})
@@ -397,13 +411,17 @@ class WebViewAudioManager(
expectedNewCommunicationDeviceId = null
- @Suppress("WakeLock", "WakeLockTimeout")
- if (device?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && proximitySensorWakeLock?.isHeld == false) {
- // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock
- proximitySensorWakeLock?.acquire()
- } else if (proximitySensorWakeLock?.isHeld == true) {
- // If the device is no longer the earpiece, we need to release the wake lock
- proximitySensorWakeLock?.release()
+ coroutineScope.launch {
+ proximitySensorMutex.withLock {
+ @Suppress("WakeLock", "WakeLockTimeout")
+ if (device?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && proximitySensorWakeLock?.isHeld == false) {
+ // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock
+ proximitySensorWakeLock?.acquire()
+ } else if (proximitySensorWakeLock?.isHeld == true) {
+ // If the device is no longer the earpiece, we need to release the wake lock
+ proximitySensorWakeLock?.release()
+ }
+ }
}
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
index f46db3ca2b6..12f8dfd00f6 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
index 55adc246e9e..f7ab2c57afc 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -133,6 +134,7 @@ class WebViewWidgetMessageInterceptor(
return assetLoader.shouldInterceptRequest(request.url)
}
+ @Suppress("OVERRIDE_DEPRECATION")
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(url.toUri())
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
index 91ea0add667..ea158c5ddca 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
index a21cadab7a8..9a489c61ce8 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
@@ -1,24 +1,27 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.utils
+import dev.zacsweers.metro.Inject
import io.element.android.features.call.impl.data.WidgetMessage
+import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
-import kotlinx.serialization.json.Json
-
-object WidgetMessageSerializer {
- private val coder = Json { ignoreUnknownKeys = true }
+@Inject
+class WidgetMessageSerializer(
+ private val json: JsonProvider,
+) {
fun deserialize(message: String): Result {
- return runCatchingExceptions { coder.decodeFromString(WidgetMessage.serializer(), message) }
+ return runCatchingExceptions { json().decodeFromString(WidgetMessage.serializer(), message) }
}
fun serialize(message: WidgetMessage): String {
- return coder.encodeToString(WidgetMessage.serializer(), message)
+ return json().encodeToString(WidgetMessage.serializer(), message)
}
}
diff --git a/features/call/impl/src/main/res/values-de/translations.xml b/features/call/impl/src/main/res/values-de/translations.xml
index 9e2ef1cea44..8fabd16ce8b 100644
--- a/features/call/impl/src/main/res/values-de/translations.xml
+++ b/features/call/impl/src/main/res/values-de/translations.xml
@@ -3,6 +3,6 @@
"Laufender Anruf""Tippen, um zum Anruf zurückzukehren""☎️ Anruf läuft"
- "In dieser Android-Version unterstützt Element Call derzeit keine Bluetooth-Audiogeräte. Bitte wählen Sie ein anderes Audiogerät aus."
+ "Element Call unterstützt in dieser Android-Version keine Bluetooth Geräte. Bitte wähle ein anderes Audiogerät.""Eingehender Element Call"
diff --git a/features/call/impl/src/main/res/values-et/translations.xml b/features/call/impl/src/main/res/values-et/translations.xml
index becc9e55657..16b72b8b973 100644
--- a/features/call/impl/src/main/res/values-et/translations.xml
+++ b/features/call/impl/src/main/res/values-et/translations.xml
@@ -1,6 +1,6 @@
- "Käimasolev kõne"
+ "Pooleliolev kõne""Kõne juurde naasmiseks klõpsa""☎️ Kõne on pooleli""Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade."
diff --git a/features/call/impl/src/main/res/values-fi/translations.xml b/features/call/impl/src/main/res/values-fi/translations.xml
index 3a7db64ae1f..16be8522969 100644
--- a/features/call/impl/src/main/res/values-fi/translations.xml
+++ b/features/call/impl/src/main/res/values-fi/translations.xml
@@ -3,5 +3,6 @@
"Käynnissä oleva puhelu""Palaa puheluun napauttamalla""☎️ Puhelu käynnissä"
+ "Element Call ei tue Bluetooth-äänilaitteiden käyttöä tässä Android-versiossa. Valitse toinen äänilaite.""Saapuva Element Call -puhelu"
diff --git a/features/call/impl/src/main/res/values-ko/translations.xml b/features/call/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..7900c57d6c3
--- /dev/null
+++ b/features/call/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "수신 중인 통화"
+ "탭해서 통화로 돌아가기"
+ "☎️ 통화 진행 중"
+ "Element Call은 이 Android 버전에서 Bluetooth 오디오 장치 사용을 지원하지 않습니다. 다른 오디오 장치를 선택하세요."
+ "Element 전화 수신"
+
diff --git a/features/call/impl/src/main/res/values-pl/translations.xml b/features/call/impl/src/main/res/values-pl/translations.xml
index e63fc80d142..d2978994e72 100644
--- a/features/call/impl/src/main/res/values-pl/translations.xml
+++ b/features/call/impl/src/main/res/values-pl/translations.xml
@@ -3,5 +3,6 @@
"Połączenie w trakcie""Stuknij, aby wrócić do rozmowy""☎️ Rozmowa w toku"
+ "Element Call nie obsługuje korzystania z urządzeń audio Bluetooth w tej wersji Androida. Wybierz inne urządzenie audio.""Przychodzące połączenie Element"
diff --git a/features/call/impl/src/main/res/values-ro/translations.xml b/features/call/impl/src/main/res/values-ro/translations.xml
index ecbccde6b4e..7d619d5978f 100644
--- a/features/call/impl/src/main/res/values-ro/translations.xml
+++ b/features/call/impl/src/main/res/values-ro/translations.xml
@@ -3,5 +3,6 @@
"Apel în curs""Atingeți pentru a reveni la apel.""☎️ Apel în curs"
+ "Element Call nu permite utilizarea dispozitivelor audio Bluetooth în această versiune Android. Vă rugăm să selectați un alt dispozitiv audio.""Primiți un apel Element Call"
diff --git a/features/call/impl/src/main/res/values-ru/translations.xml b/features/call/impl/src/main/res/values-ru/translations.xml
index 6bf3ebbb946..e7de6d32881 100644
--- a/features/call/impl/src/main/res/values-ru/translations.xml
+++ b/features/call/impl/src/main/res/values-ru/translations.xml
@@ -3,5 +3,6 @@
"Текущий вызов""Коснитесь, чтобы вернуться к вызову""☎️ Идёт вызов"
+ "Функция Element Call не поддерживает использование аудиоустройств Bluetooth в данной версии Android. Пожалуйста, выберите другое аудиоустройство.""Входящий вызов Element"
diff --git a/features/call/impl/src/main/res/values-sv/translations.xml b/features/call/impl/src/main/res/values-sv/translations.xml
index b5ee7bc2dac..930b5eec6f8 100644
--- a/features/call/impl/src/main/res/values-sv/translations.xml
+++ b/features/call/impl/src/main/res/values-sv/translations.xml
@@ -3,5 +3,6 @@
"Pågående samtal""Tryck för att återgå till samtalet""☎️ Samtal pågår"
+ "Element Call stöder inte användning av Bluetooth-ljudenheter i den här Android-versionen. Välj en annan ljudenhet.""Inkommande Element Call"
diff --git a/features/call/impl/src/main/res/values-uz/translations.xml b/features/call/impl/src/main/res/values-uz/translations.xml
index 010695a2ef7..ceab664cc2f 100644
--- a/features/call/impl/src/main/res/values-uz/translations.xml
+++ b/features/call/impl/src/main/res/values-uz/translations.xml
@@ -2,5 +2,7 @@
"Davom etayotgan qo\'ng\'iroq""Qo\'ng\'iroqqa qaytish uchun bosing"
- "☎️ Qo‘ng‘iroq davom etmoqda"
+ "☎️ Qoʻngʻiroq davom etmoqda"
+ "Element Call ushbu Android versiyasida Bluetooth audio qurilmalaridan foydalanishni qoʻllab-quvvatlamaydi. Iltimos, boshqa audio qurilmani tanlang."
+ "Kiruvchi element qoʻngʻirogʻi"
diff --git a/features/call/impl/src/main/res/values-zh-rTW/translations.xml b/features/call/impl/src/main/res/values-zh-rTW/translations.xml
index ec01e5a6e31..d0e400dd07f 100644
--- a/features/call/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/call/impl/src/main/res/values-zh-rTW/translations.xml
@@ -3,5 +3,6 @@
"進行中的通話""點擊以返回到通話頁面""☎️ 通話中"
+ "Element Call 不支援在此 Android 版本中使用藍牙音訊裝置。請選取其他音訊裝置。""Element 來電"
diff --git a/features/call/impl/src/main/res/values-zh/translations.xml b/features/call/impl/src/main/res/values-zh/translations.xml
index 8eda0df2126..6192568a61c 100644
--- a/features/call/impl/src/main/res/values-zh/translations.xml
+++ b/features/call/impl/src/main/res/values-zh/translations.xml
@@ -3,5 +3,6 @@
"通话进行中""点按即可返回通话""☎️ 通话中"
+ "Element Call 不支持在此 Android 版本中使用蓝牙音频设备。请选择其他音频设备。""Element 来电"
diff --git a/features/call/impl/src/main/res/values/do_not_translate.xml b/features/call/impl/src/main/res/values/do_not_translate.xml
index d998d4b1c69..deb835b35cc 100644
--- a/features/call/impl/src/main/res/values/do_not_translate.xml
+++ b/features/call/impl/src/main/res/values/do_not_translate.xml
@@ -1,7 +1,8 @@
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt
index 86d9b80d653..bfc6565d113 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -59,6 +60,7 @@ class DefaultElementCallEntryPointTest {
senderName = "senderName",
avatarUrl = "avatarUrl",
timestamp = 0,
+ expirationTimestamp = 0,
notificationChannelId = "notificationChannelId",
textContent = "textContent",
)
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
index 57de8261649..e3563584378 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
index 9c5406a8a51..5153f79c31a 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipSupportProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipSupportProvider.kt
index 43fc0a562af..c17e31261cf 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipSupportProvider.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipSupportProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
index a15fbbefa0b..55e94312bfc 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt
index d870382e484..c087fa3c35c 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt
index f36267a353e..28e2747ba9d 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,13 +13,14 @@ import androidx.test.platform.app.InstrumentationRegistry
import coil3.ImageLoader
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.test.AN_EVENT_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_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
-import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
+import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
@@ -52,7 +54,7 @@ class RingingCallNotificationCreatorTest {
@Test
fun `createNotification - tries to load the avatar URL`() = runTest {
- val getUserIconLambda = lambdaRecorder { _, _ -> null }
+ val getUserIconLambda = lambdaRecorder { _, _ -> null }
val notificationCreator = createRingingCallNotificationCreator(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda)
@@ -73,6 +75,7 @@ class RingingCallNotificationCreatorTest {
roomAvatarUrl = "https://example.com/avatar.jpg",
notificationChannelId = "channelId",
timestamp = 0L,
+ expirationTimestamp = 20L,
textContent = "textContent",
)
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
index 7709066dd04..09aaaf82717 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,9 +17,11 @@ import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.ui.CallScreenEvents
import io.element.android.features.call.impl.ui.CallScreenNavigator
import io.element.android.features.call.impl.ui.CallScreenPresenter
+import io.element.android.features.call.impl.utils.WidgetMessageSerializer
import io.element.android.features.call.utils.FakeActiveCallManager
import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
+import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState
@@ -26,13 +29,11 @@ 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.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
-import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker
-import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
@@ -52,7 +53,8 @@ import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
-@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest {
+@OptIn(ExperimentalCoroutinesApi::class)
+class CallScreenPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -82,19 +84,12 @@ import kotlin.time.Duration.Companion.seconds
}
@Test
- fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
- val sendCallNotificationIfNeededLambda = lambdaRecorder> { Result.success(true) }
- val syncService = FakeSyncService(SyncState.Running)
- val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
- val client = FakeMatrixClient(syncService = syncService).apply {
- givenGetRoomResult(A_ROOM_ID, fakeRoom)
- }
+ fun `present - with CallType RoomCall sets call as active, loads URL and runs WidgetDriver`() = runTest {
val widgetDriver = FakeMatrixWidgetDriver()
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
val analyticsLambda = lambdaRecorder {}
val joinedCallLambda = lambdaRecorder {}
val presenter = createCallScreenPresenter(
- matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
widgetProvider = widgetProvider,
@@ -116,7 +111,6 @@ import kotlin.time.Duration.Companion.seconds
assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
- sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
// Wait until the WidgetDriver is loaded
skipItems(1)
@@ -225,7 +219,7 @@ import kotlin.time.Duration.Companion.seconds
}
@Test
- fun `present - a received 'joined' action makes the call to be active`() = runTest {
+ fun `present - a received 'content loaded' action makes the call to be active`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
@@ -248,7 +242,7 @@ import kotlin.time.Duration.Companion.seconds
messageInterceptor.givenInterceptedMessage(
"""
{
- "action":"io.element.join",
+ "action":"content_loaded",
"api":"fromWidget",
"widgetId":"1",
"requestId":"1"
@@ -399,7 +393,6 @@ import kotlin.time.Duration.Companion.seconds
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
- activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@@ -420,7 +413,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
- activeRoomsHolder = activeRoomsHolder,
+ widgetMessageSerializer = WidgetMessageSerializer(DefaultJsonProvider()),
)
}
}
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt
new file mode 100644
index 00000000000..0c91b2159a8
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.call.ui
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.ui.getSessionId
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import org.junit.Test
+
+class CallTypeTest {
+ @Test
+ fun `getSessionId returns null for ExternalUrl`() {
+ assertThat(CallType.ExternalUrl("aURL").getSessionId()).isNull()
+ }
+
+ @Test
+ fun `getSessionId returns the sessionId for RoomCall`() {
+ assertThat(
+ CallType.RoomCall(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ ).getSessionId()
+ ).isEqualTo(A_SESSION_ID)
+ }
+
+ @Test
+ fun `ExternalUrl stringification does not contain the URL`() {
+ assertThat(CallType.ExternalUrl("aURL").toString()).isEqualTo("ExternalUrl")
+ }
+
+ @Test
+ fun `RoomCall stringification does not contain the URL`() {
+ assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString())
+ .isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)")
+ }
+}
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
index 3e1d47bb574..431b7463624 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeLanguageTagProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeLanguageTagProvider.kt
index 5f80db15407..f68842813b3 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeLanguageTagProvider.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeLanguageTagProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
index bafbe3f5703..43f7f931f15 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
index 84084c38fe2..df14b4b4236 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,22 +23,29 @@ import io.element.android.features.call.test.aCallNotificationData
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.SessionId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
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_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
+import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
+import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
-import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
+import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
+import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.plantTestTimber
+import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -164,6 +172,102 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.cancel(notificationId) }
}
+ @Test
+ fun `Decline event - Hangup on a ringing call should send a decline event`() = runTest {
+ setupShadowPowerManager()
+ val notificationManagerCompat = mockk(relaxed = true)
+
+ val room = mockk(relaxed = true)
+
+ val matrixClient = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, room)
+ }
+ val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
+
+ val manager = createActiveCallManager(
+ matrixClientProvider = clientProvider,
+ notificationManagerCompat = notificationManagerCompat
+ )
+
+ val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
+ manager.registerIncomingCall(notificationData)
+
+ manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
+
+ coVerify {
+ room.declineCall(notificationEventId = notificationData.eventId)
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `Decline event - Declining from another session should stop ringing`() = runTest {
+ setupShadowPowerManager()
+ val notificationManagerCompat = mockk(relaxed = true)
+
+ val room = FakeJoinedRoom()
+
+ val matrixClient = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, room)
+ }
+ val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
+
+ val manager = createActiveCallManager(
+ matrixClientProvider = clientProvider,
+ notificationManagerCompat = notificationManagerCompat
+ )
+
+ val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
+ manager.registerIncomingCall(notificationData)
+
+ runCurrent()
+
+ // Simulate declined from other session
+ room.baseRoom.givenDecliner(matrixClient.sessionId, notificationData.eventId)
+
+ runCurrent()
+
+ assertThat(manager.activeCall.value).isNull()
+ assertThat(manager.activeWakeLock?.isHeld).isFalse()
+
+ verify { notificationManagerCompat.cancel(notificationId) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `Decline event - Should ignore decline for other notification events`() = runTest {
+ plantTestTimber()
+ setupShadowPowerManager()
+ val notificationManagerCompat = mockk(relaxed = true)
+
+ val room = FakeJoinedRoom()
+
+ val matrixClient = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, room)
+ }
+ val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
+
+ val manager = createActiveCallManager(
+ matrixClientProvider = clientProvider,
+ notificationManagerCompat = notificationManagerCompat
+ )
+
+ val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
+ manager.registerIncomingCall(notificationData)
+
+ runCurrent()
+
+ // Simulate declined for another notification event
+ room.baseRoom.givenDecliner(matrixClient.sessionId, AN_EVENT_ID_2)
+
+ runCurrent()
+
+ assertThat(manager.activeCall.value).isNotNull()
+ assertThat(manager.activeWakeLock?.isHeld).isTrue()
+
+ verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
+ }
+
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
setupShadowPowerManager()
@@ -267,6 +371,84 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNotNull()
}
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `IncomingCall - rings no longer than expiration time`() = runTest {
+ setupShadowPowerManager()
+ val notificationManagerCompat = mockk(relaxed = true)
+ val clock = FakeSystemClock()
+ val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
+
+ assertThat(manager.activeWakeLock?.isHeld).isFalse()
+ assertThat(manager.activeCall.value).isNull()
+
+ val eventTimestamp = A_FAKE_TIMESTAMP
+ // The call should not ring more than 30 seconds after the initial event was sent
+ val expirationTimestamp = eventTimestamp + 30_000
+
+ val callNotificationData = aCallNotificationData(
+ timestamp = eventTimestamp,
+ expirationTimestamp = expirationTimestamp,
+ )
+
+ // suppose it took 10s to be notified
+ clock.epochMillisResult = eventTimestamp + 10_000
+ manager.registerIncomingCall(callNotificationData)
+
+ assertThat(manager.activeCall.value).isEqualTo(
+ ActiveCall(
+ callType = CallType.RoomCall(
+ sessionId = callNotificationData.sessionId,
+ roomId = callNotificationData.roomId,
+ ),
+ callState = CallState.Ringing(callNotificationData)
+ )
+ )
+
+ runCurrent()
+
+ assertThat(manager.activeWakeLock?.isHeld).isTrue()
+ verify { notificationManagerCompat.notify(notificationId, any()) }
+
+ // advance by 21s it should have stopped ringing
+ advanceTimeBy(21_000)
+ runCurrent()
+
+ verify { notificationManagerCompat.cancel(any()) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `IncomingCall - ignore expired ring lifetime`() = runTest {
+ setupShadowPowerManager()
+ val notificationManagerCompat = mockk(relaxed = true)
+ val clock = FakeSystemClock()
+ val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
+
+ assertThat(manager.activeWakeLock?.isHeld).isFalse()
+ assertThat(manager.activeCall.value).isNull()
+
+ val eventTimestamp = A_FAKE_TIMESTAMP
+ // The call should not ring more than 30 seconds after the initial event was sent
+ val expirationTimestamp = eventTimestamp + 30_000
+
+ val callNotificationData = aCallNotificationData(
+ timestamp = eventTimestamp,
+ expirationTimestamp = expirationTimestamp,
+ )
+
+ // suppose it took 35s to be notified
+ clock.epochMillisResult = eventTimestamp + 35_000
+ manager.registerIncomingCall(callNotificationData)
+
+ assertThat(manager.activeCall.value).isNull()
+
+ runCurrent()
+
+ assertThat(manager.activeWakeLock?.isHeld).isFalse()
+ verify(exactly = 0) { notificationManagerCompat.notify(notificationId, any()) }
+ }
+
private fun setupShadowPowerManager() {
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService()).apply {
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
@@ -277,6 +459,7 @@ class DefaultActiveCallManagerTest {
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
+ systemClock: FakeSystemClock = FakeSystemClock(),
) = DefaultActiveCallManager(
context = InstrumentationRegistry.getInstrumentation().targetContext,
coroutineScope = backgroundScope,
@@ -292,5 +475,6 @@ class DefaultActiveCallManagerTest {
defaultCurrentCallService = DefaultCurrentCallService(),
appForegroundStateService = FakeAppForegroundStateService(),
imageLoaderHolder = FakeImageLoaderHolder(),
+ systemClock = systemClock,
)
}
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
index a93dd5f91c9..95d53987047 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,6 +23,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
+import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -85,7 +87,7 @@ class DefaultCallWidgetProviderTest {
// No room from the client
givenGetRoomResult(A_ROOM_ID, null)
}
- val activeRoomsHolder = ActiveRoomsHolder().apply {
+ val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
// A current active room with the same room id
addRoom(
FakeJoinedRoom(
@@ -129,7 +131,7 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
- activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
+ activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
) = DefaultCallWidgetProvider(
matrixClientsProvider = matrixClientProvider,
appPreferencesStore = appPreferencesStore,
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt
index 90527370e7b..74bd1c36ac5 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
index 257b92b7090..11e6d9e3991 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
index c41339b8dfc..6b68b1519c1 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts
index 4fe9b95384f..76fbf9915e3 100644
--- a/features/call/test/build.gradle.kts
+++ b/features/call/test/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt
index 8e6ee00a169..2c7d1914c74 100644
--- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt
+++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -30,6 +31,7 @@ fun aCallNotificationData(
avatarUrl: String? = AN_AVATAR_URL,
notificationChannelId: String = "channel_id",
timestamp: Long = 0L,
+ expirationTimestamp: Long = 30_000L,
textContent: String? = null,
): CallNotificationData = CallNotificationData(
sessionId = sessionId,
@@ -41,5 +43,6 @@ fun aCallNotificationData(
avatarUrl = avatarUrl,
notificationChannelId = notificationChannelId,
timestamp = timestamp,
+ expirationTimestamp = expirationTimestamp,
textContent = textContent,
)
diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt
index 995b0d0a547..45f527720c4 100644
--- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt
+++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt
index 09a12692597..fdf3ca566b6 100644
--- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt
+++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -38,6 +39,7 @@ class FakeElementCallEntryPoint(
senderName: String?,
avatarUrl: String?,
timestamp: Long,
+ expirationTimestamp: Long,
notificationChannelId: String,
textContent: String?,
) {
diff --git a/features/createroom/api/build.gradle.kts b/features/createroom/api/build.gradle.kts
index 7f39051b0e6..b4d7d2a9952 100644
--- a/features/createroom/api/build.gradle.kts
+++ b/features/createroom/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
index 3b35d0a3e1a..1c6a9f04db9 100644
--- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
+++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,17 +12,16 @@ import com.bumble.appyx.core.modality.BuildContext
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.matrix.api.core.RoomIdOrAlias
+import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
- fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
- interface NodeBuilder {
- fun callback(callback: Callback): NodeBuilder
- fun build(): Node
- }
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ callback: Callback,
+ ): Node
interface Callback : Plugin {
- fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List)
- fun onOpenRoomDirectory()
+ fun onRoomCreated(roomId: RoomId)
}
}
diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts
index 769f7b56286..1adaac1f6da 100644
--- a/features/createroom/impl/build.gradle.kts
+++ b/features/createroom/impl/build.gradle.kts
@@ -1,10 +1,11 @@
-import extension.ComponentMergingStrategy
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -23,7 +24,7 @@ android {
}
}
-setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
+setupDependencyInjection()
dependencies {
implementation(projects.libraries.core)
@@ -33,7 +34,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
@@ -41,24 +42,16 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
+ implementation(projects.features.invitepeople.api)
api(projects.features.createroom.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.test.mockk)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
+ testCommonDependencies(libs, true)
testImplementation(projects.services.analytics.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.usersearch.test)
- testImplementation(projects.features.createroom.test)
+ testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt
deleted file mode 100644
index 69eac7d3694..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom
-
-import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.navmodel.backstack.BackStack
-import com.bumble.appyx.navmodel.backstack.operation.push
-import io.element.android.features.createroom.impl.CreateRoomFlowNode.NavTarget
-import io.element.android.libraries.architecture.overlay.Overlay
-import io.element.android.libraries.architecture.overlay.operation.hide
-import io.element.android.libraries.architecture.overlay.operation.show
-import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-
-interface CreateRoomNavigator : Plugin {
- fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List)
- fun onCreateNewRoom()
- fun onShowJoinRoomByAddress()
- fun onDismissJoinRoomByAddress()
- fun onOpenRoomDirectory()
-}
-
-class DefaultCreateRoomNavigator(
- private val backstack: BackStack,
- private val overlay: Overlay,
- private val openRoom: (RoomIdOrAlias, List) -> Unit,
- private val openRoomDirectory: () -> Unit,
-) : CreateRoomNavigator {
- override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) = openRoom(roomIdOrAlias, serverNames)
-
- override fun onOpenRoomDirectory() = openRoomDirectory()
-
- override fun onCreateNewRoom() {
- backstack.push(NavTarget.NewRoom)
- }
-
- override fun onShowJoinRoomByAddress() {
- overlay.show(NavTarget.JoinByAddress)
- }
-
- override fun onDismissJoinRoomByAddress() {
- overlay.hide()
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt
deleted file mode 100644
index a7678f130f7..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl
-
-import android.os.Parcelable
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import com.bumble.appyx.navmodel.backstack.BackStack
-import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
-import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
-import io.element.android.features.createroom.impl.di.CreateRoomComponent
-import io.element.android.libraries.architecture.BackstackView
-import io.element.android.libraries.architecture.BaseFlowNode
-import io.element.android.libraries.architecture.bindings
-import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.DaggerComponentOwner
-import io.element.android.libraries.di.SessionScope
-import kotlinx.parcelize.Parcelize
-
-@ContributesNode(SessionScope::class)
-class ConfigureRoomFlowNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
-) : DaggerComponentOwner,
- BaseFlowNode(
- backstack = BackStack(
- initialElement = NavTarget.Root,
- savedStateMap = buildContext.savedStateMap,
- ),
- buildContext = buildContext,
- plugins = plugins
- ) {
- private val component by lazy {
- parent!!.bindings().createRoomComponentBuilder().build()
- }
- private val navigator = plugins().first()
-
- override val daggerComponent: Any
- get() = component
-
- sealed interface NavTarget : Parcelable {
- @Parcelize
- data object Root : NavTarget
-
- @Parcelize
- data object ConfigureRoom : NavTarget
- }
-
- override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
- return when (navTarget) {
- NavTarget.Root -> {
- val callback = object : AddPeopleNode.Callback {
- override fun onContinue() {
- backstack.push(NavTarget.ConfigureRoom)
- }
- }
- createNode(buildContext = buildContext, plugins = listOf(callback))
- }
- NavTarget.ConfigureRoom -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
- }
- }
- }
-
- @Composable
- override fun View(modifier: Modifier) {
- BackstackView()
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt
deleted file mode 100644
index 4cfa6158b8e..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl
-
-import android.net.Uri
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
-import io.element.android.libraries.matrix.api.user.MatrixUser
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-
-data class CreateRoomConfig(
- val roomName: String? = null,
- val topic: String? = null,
- val avatarUri: Uri? = null,
- val invites: ImmutableList = persistentListOf(),
- val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
-)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt
deleted file mode 100644
index 29b1525f511..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl
-
-import android.net.Uri
-import io.element.android.features.createroom.impl.configureroom.RoomAccess
-import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
-import io.element.android.features.createroom.impl.configureroom.RoomAddress
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
-import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
-import io.element.android.features.createroom.impl.di.CreateRoomScope
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.libraries.androidutils.file.safeDelete
-import io.element.android.libraries.di.SingleIn
-import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
-import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.getAndUpdate
-import java.io.File
-import javax.inject.Inject
-
-@SingleIn(CreateRoomScope::class)
-class CreateRoomDataStore @Inject constructor(
- val selectedUserListDataStore: UserListDataStore,
- private val roomAliasHelper: RoomAliasHelper,
-) {
- private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig())
- private var cachedAvatarUri: Uri? = null
- set(value) {
- field?.path?.let { File(it) }?.safeDelete()
- field = value
- }
-
- val createRoomConfigWithInvites: Flow = combine(
- selectedUserListDataStore.selectedUsers,
- createRoomConfigFlow,
- ) { selectedUsers, config ->
- config.copy(invites = selectedUsers.toImmutableList())
- }
-
- fun setRoomName(roomName: String) {
- createRoomConfigFlow.getAndUpdate { config ->
- val newVisibility = when (config.roomVisibility) {
- is RoomVisibilityState.Public -> {
- val roomAddress = config.roomVisibility.roomAddress
- if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
- val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
- config.roomVisibility.copy(
- roomAddress = RoomAddress.AutoFilled(roomAliasName),
- )
- } else {
- config.roomVisibility
- }
- }
- else -> config.roomVisibility
- }
- config.copy(
- roomName = roomName.takeIf { it.isNotEmpty() },
- roomVisibility = newVisibility,
- )
- }
- }
-
- fun setTopic(topic: String) {
- createRoomConfigFlow.getAndUpdate { config ->
- config.copy(topic = topic.takeIf { it.isNotEmpty() })
- }
- }
-
- fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
- cachedAvatarUri = uri.takeIf { cached }
- createRoomConfigFlow.getAndUpdate { config ->
- config.copy(avatarUri = uri)
- }
- }
-
- fun setRoomVisibility(visibility: RoomVisibilityItem) {
- createRoomConfigFlow.getAndUpdate { config ->
- config.copy(
- roomVisibility = when (visibility) {
- RoomVisibilityItem.Private -> RoomVisibilityState.Private
- RoomVisibilityItem.Public -> {
- val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
- RoomVisibilityState.Public(
- roomAddress = RoomAddress.AutoFilled(roomAliasName),
- roomAccess = RoomAccess.Anyone,
- )
- }
- }
- )
- }
- }
-
- fun setRoomAddress(address: String) {
- createRoomConfigFlow.getAndUpdate { config ->
- config.copy(
- roomVisibility = when (config.roomVisibility) {
- is RoomVisibilityState.Public -> {
- val sanitizedAddress = address.lowercase()
- config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
- }
- else -> config.roomVisibility
- }
- )
- }
- }
-
- fun setRoomAccess(access: RoomAccessItem) {
- createRoomConfigFlow.getAndUpdate { config ->
- config.copy(
- roomVisibility = when (config.roomVisibility) {
- is RoomVisibilityState.Public -> {
- when (access) {
- RoomAccessItem.Anyone -> config.roomVisibility.copy(roomAccess = RoomAccess.Anyone)
- RoomAccessItem.AskToJoin -> config.roomVisibility.copy(roomAccess = RoomAccess.Knocking)
- }
- }
- else -> config.roomVisibility
- }
- )
- }
- }
-
- fun clearCachedData() {
- cachedAvatarUri = null
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt
index b8b755a0b33..7fea6fc0e50 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt
@@ -1,90 +1,82 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl
import android.os.Parcelable
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.DefaultCreateRoomNavigator
+import com.bumble.appyx.navmodel.backstack.operation.replace
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
-import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode
-import io.element.android.features.createroom.impl.root.CreateRoomRootNode
+import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
-import io.element.android.libraries.architecture.OverlayView
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class CreateRoomFlowNode @AssistedInject constructor(
+@AssistedInject
+class CreateRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
) : BaseFlowNode(
backstack = BackStack(
- initialElement = NavTarget.Root,
+ initialElement = NavTarget.ConfigureRoom,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
- sealed interface NavTarget : Parcelable {
- @Parcelize
- data object Root : NavTarget
-
- @Parcelize
- data object NewRoom : NavTarget
-
- @Parcelize
- data object JoinByAddress : NavTarget
- }
-
- private val navigator = DefaultCreateRoomNavigator(
- backstack = backstack,
- overlay = overlay,
- openRoom = { roomIdOrAlias, viaServers ->
- plugins().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
- },
- openRoomDirectory = {
- plugins().forEach { it.onOpenRoomDirectory() }
- }
- )
+ private val callback: CreateRoomEntryPoint.Callback = callback()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
- NavTarget.Root -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
+ NavTarget.ConfigureRoom -> {
+ val callback = object : ConfigureRoomNode.Callback {
+ override fun onCreateRoomSuccess(roomId: RoomId) {
+ backstack.replace(NavTarget.AddPeople(roomId))
+ }
+ }
+ createNode(buildContext, plugins = listOf(callback))
}
- NavTarget.NewRoom -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
- }
- NavTarget.JoinByAddress -> {
- createNode(buildContext = buildContext, plugins = listOf(navigator))
+ is NavTarget.AddPeople -> {
+ val inputs = AddPeopleNode.Inputs(navTarget.roomId)
+ val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback {
+ override fun onFinish() {
+ callback.onRoomCreated(navTarget.roomId)
+ }
+ }
+ createNode(buildContext, plugins = listOf(inputs, callback))
}
}
}
@Composable
override fun View(modifier: Modifier) {
- Box(modifier = modifier) {
- BackstackView()
- OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
- }
+ BackstackView()
+ }
+
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ data object ConfigureRoom : NavTarget
+
+ @Parcelize
+ data class AddPeople(val roomId: RoomId) : NavTarget
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
index 161d67e817a..2261d294cfa 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,27 +10,18 @@ package io.element.android.features.createroom.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
+import io.element.android.libraries.di.SessionScope
-@ContributesBinding(AppScope::class)
-class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
- val plugins = ArrayList()
-
- return object : CreateRoomEntryPoint.NodeBuilder {
- override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
- plugins += callback
- return this
- }
-
- override fun build(): Node {
- return parentNode.createNode(buildContext, plugins)
- }
- }
+@ContributesBinding(SessionScope::class)
+class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ callback: CreateRoomEntryPoint.Callback,
+ ): Node {
+ return parentNode.createNode(buildContext, listOf(callback))
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt
index ba9eb820739..2e5c16e62c1 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,34 +13,48 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.impl.di.CreateRoomScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.features.invitepeople.api.InvitePeoplePresenter
+import io.element.android.features.invitepeople.api.InvitePeopleRenderer
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
-@ContributesNode(CreateRoomScope::class)
-class AddPeopleNode @AssistedInject constructor(
+@ContributesNode(SessionScope::class)
+@AssistedInject
+class AddPeopleNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: AddPeoplePresenter,
+ invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
+ private val invitePeopleRenderer: InvitePeopleRenderer,
) : Node(buildContext, plugins = plugins) {
+ data class Inputs(
+ val roomId: RoomId,
+ ) : NodeInputs
+
interface Callback : Plugin {
- fun onContinue()
+ fun onFinish()
}
- private fun onContinue() {
- plugins().forEach { it.onContinue() }
- }
+ private val callback: Callback = callback()
+ private val roomId = inputs().roomId
+ private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
+ joinedRoom = null,
+ roomId = roomId,
+ )
@Composable
override fun View(modifier: Modifier) {
- val state = presenter.present()
+ val state = invitePeoplePresenter.present()
AddPeopleView(
state = state,
- modifier = modifier,
- onBackClick = this::navigateUp,
- onNextClick = this::onContinue,
- )
+ onFinish = callback::onFinish,
+ ) {
+ invitePeopleRenderer.Render(state, Modifier)
+ }
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt
deleted file mode 100644
index e050a0738ec..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import androidx.compose.runtime.Composable
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListPresenter
-import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.usersearch.api.UserRepository
-import javax.inject.Inject
-
-class AddPeoplePresenter @Inject constructor(
- userListPresenterFactory: UserListPresenter.Factory,
- userRepository: UserRepository,
- dataStore: CreateRoomDataStore,
-) : Presenter {
- private val userListPresenter = userListPresenterFactory.create(
- UserListPresenterArgs(
- selectionMode = SelectionMode.Multiple,
- ),
- userRepository,
- dataStore.selectedUserListDataStore,
- )
-
- @Composable
- override fun present(): UserListState {
- return userListPresenter.present()
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt
deleted file mode 100644
index 9e0ddd04c82..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
-import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
-import io.element.android.libraries.matrix.ui.components.aMatrixUserList
-import io.element.android.libraries.usersearch.api.UserSearchResult
-import kotlinx.collections.immutable.toImmutableList
-
-open class AddPeopleUserListStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aUserListState(),
- aUserListState(
- searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
- selectedUsers = aMatrixUserList().toImmutableList(),
- isSearchActive = false,
- selectionMode = SelectionMode.Multiple,
- ),
- aUserListState(
- searchResults = SearchBarResultState.Results(
- aMatrixUserList()
- .mapIndexed { index, matrixUser ->
- UserSearchResult(matrixUser, index % 2 == 0)
- }
- .toImmutableList()
- ),
- selectedUsers = aMatrixUserList().toImmutableList(),
- isSearchActive = true,
- selectionMode = SelectionMode.Multiple,
- ),
- aUserListState(
- recentDirectRooms = aRecentDirectRoomList(),
- ),
- )
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
index d39ac4d2500..0a7309a2b0a 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
@@ -1,83 +1,75 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.addpeople
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.components.UserListView
-import io.element.android.features.createroom.impl.userlist.UserListEvents
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AddPeopleView(
- state: UserListState,
- onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+ state: InvitePeopleState,
+ onFinish: () -> Unit,
modifier: Modifier = Modifier,
+ invitePeopleView: @Composable () -> Unit,
) {
- Scaffold(
+ HeaderFooterPage(
modifier = modifier,
+ contentPadding = PaddingValues(0.dp),
topBar = {
- AddPeopleViewTopBar(
- hasSelectedUsers = state.selectedUsers.isNotEmpty(),
- onBackClick = {
- if (state.isSearchActive) {
- state.eventSink(UserListEvents.OnSearchActiveChanged(false))
- } else {
- onBackClick()
- }
+ AddPeopleTopBar(onSkipClick = onFinish)
+ },
+ footer = {
+ Button(
+ text = stringResource(CommonStrings.action_finish),
+ onClick = {
+ state.eventSink(InvitePeopleEvents.SendInvites)
+ onFinish()
},
- onNextClick = onNextClick,
+ enabled = state.canInvite,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
)
- }
- ) { padding ->
- UserListView(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- .consumeWindowInsets(padding),
- state = state,
- showBackButton = false,
- onSelectUser = {},
- onDeselectUser = {},
- )
- }
+ },
+ content = invitePeopleView
+ )
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun AddPeopleViewTopBar(
- hasSelectedUsers: Boolean,
- onBackClick: () -> Unit,
- onNextClick: () -> Unit,
+private fun AddPeopleTopBar(
+ onSkipClick: () -> Unit,
) {
TopAppBar(
- titleStr = stringResource(id = R.string.screen_create_room_add_people_title),
- navigationIcon = { BackButton(onClick = onBackClick) },
+ titleStr = stringResource(R.string.screen_create_room_add_people_title),
actions = {
- val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton(
- text = stringResource(id = textActionResId),
- onClick = onNextClick,
+ text = stringResource(CommonStrings.action_skip),
+ onClick = onSkipClick,
)
}
)
@@ -85,10 +77,10 @@ private fun AddPeopleViewTopBar(
@PreviewsDayNight
@Composable
-internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
+internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
AddPeopleView(
state = state,
- onBackClick = {},
- onNextClick = {},
+ invitePeopleView = {},
+ onFinish = {},
)
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
index 6885123f633..fed84041431 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
@@ -1,13 +1,13 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
-import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
@@ -16,7 +16,6 @@ sealed interface ConfigureRoomEvents {
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
- data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
index e7188251633..43ceee3594e 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,24 +14,26 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.features.createroom.impl.di.CreateRoomScope
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.callback
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
-@ContributesNode(CreateRoomScope::class)
-class ConfigureRoomNode @AssistedInject constructor(
+@ContributesNode(SessionScope::class)
+@AssistedInject
+class ConfigureRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: ConfigureRoomPresenter,
private val analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
+ interface Callback : Plugin {
+ fun onCreateRoomSuccess(roomId: RoomId)
+ }
init {
lifecycle.subscribe(
@@ -40,6 +43,8 @@ class ConfigureRoomNode @AssistedInject constructor(
)
}
+ private val callback: Callback = callback()
+
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -47,9 +52,7 @@ class ConfigureRoomNode @AssistedInject constructor(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
- onCreateRoomSuccess = {
- navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
- },
+ onCreateRoomSuccess = callback::onCreateRoomSuccess,
)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
index 0025a64df37..c5d68e127f4 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -17,9 +18,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.core.net.toUri
+import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
import io.element.android.libraries.mediapickers.api.PickerProvider
+import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
@@ -45,11 +47,11 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
-import javax.inject.Inject
import kotlin.jvm.optionals.getOrDefault
-class ConfigureRoomPresenter @Inject constructor(
- private val dataStore: CreateRoomDataStore,
+@Inject
+class ConfigureRoomPresenter(
+ private val dataStore: CreateRoomConfigStore,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
@@ -57,6 +59,7 @@ class ConfigureRoomPresenter @Inject constructor(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val featureFlagService: FeatureFlagService,
private val roomAliasHelper: RoomAliasHelper,
+ private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) : Presenter {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
@@ -64,7 +67,7 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
- val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
+ val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
@@ -114,12 +117,11 @@ class ConfigureRoomPresenter @Inject constructor(
localCoroutineScope.createRoom(config, createRoomAction)
}
- fun handleEvents(event: ConfigureRoomEvents) {
+ fun handleEvent(event: ConfigureRoomEvents) {
when (event) {
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
- is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
@@ -148,7 +150,7 @@ class ConfigureRoomPresenter @Inject constructor(
cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity.value,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
@@ -157,7 +159,7 @@ class ConfigureRoomPresenter @Inject constructor(
createRoomAction: MutableState>
) = launch {
suspend {
- val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
+ val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
CreateRoomParameters(
name = config.roomName,
@@ -201,9 +203,9 @@ class ConfigureRoomPresenter @Inject constructor(
uri = avatarUri,
mimeType = MimeTypes.Jpeg,
deleteOriginal = false,
- compressIfPossible = false,
+ mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
).getOrThrow()
val byteArray = preprocessed.file.readBytes()
- return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray, null).getOrThrow()
+ return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray).getOrThrow()
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt
deleted file mode 100644
index b41b98b8cc0..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.configureroom
-
-import io.element.android.libraries.matrix.api.user.MatrixUser
-
-data class ConfigureRoomPresenterArgs(
- val selectedUsers: List,
-)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
index 6651d16604c..127aa20ccb1 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
@@ -1,13 +1,13 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
-import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
index 71568dbbc76..7f760b46fba 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
@@ -1,14 +1,14 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
index 9c468539ab8..6715a7c1e9c 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
@@ -1,18 +1,17 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
-import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -30,6 +29,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -56,7 +57,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
-import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
import io.element.android.libraries.permissions.api.PermissionsView
@@ -110,16 +110,6 @@ fun ConfigureRoomView(
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
- if (state.config.invites.isNotEmpty()) {
- SelectedUsersRowList(
- contentPadding = PaddingValues(horizontal = 24.dp),
- selectedUsers = state.config.invites,
- onUserRemove = {
- focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(it))
- },
- )
- }
RoomVisibilityOptions(
selected = when (state.config.roomVisibility) {
is RoomVisibilityState.Private -> RoomVisibilityItem.Private
@@ -202,7 +192,7 @@ private fun ConfigureRoomToolbar(
@Composable
private fun RoomNameWithAvatar(
- avatarUri: Uri?,
+ avatarUri: String?,
roomName: String,
onAvatarClick: () -> Unit,
onChangeRoomName: (String) -> Unit,
@@ -213,11 +203,19 @@ private fun RoomNameWithAvatar(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
+ val a11yAvatar = stringResource(CommonStrings.a11y_room_avatar)
UnsavedAvatar(
avatarUri = avatarUri,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = AvatarType.Room(),
- modifier = Modifier.clickable(onClick = onAvatarClick),
+ modifier = Modifier
+ .clickable(
+ onClick = onAvatarClick,
+ onClickLabel = stringResource(CommonStrings.action_open_context_menu),
+ )
+ .clearAndSetSemantics {
+ contentDescription = a11yAvatar
+ },
)
TextField(
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt
new file mode 100644
index 00000000000..b9d05a144fe
--- /dev/null
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfig.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.createroom.impl.configureroom
+
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+data class CreateRoomConfig(
+ val roomName: String? = null,
+ val topic: String? = null,
+ val avatarUri: String? = null,
+ val invites: ImmutableList = persistentListOf(),
+ val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
+)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt
new file mode 100644
index 00000000000..5e8637ddd64
--- /dev/null
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/CreateRoomConfigStore.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.createroom.impl.configureroom
+
+import android.net.Uri
+import dev.zacsweers.metro.Inject
+import io.element.android.libraries.androidutils.file.safeDelete
+import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
+import java.io.File
+
+@Inject
+class CreateRoomConfigStore(
+ private val roomAliasHelper: RoomAliasHelper,
+) {
+ private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig())
+
+ private var cachedAvatarUri: Uri? = null
+ set(value) {
+ field?.path?.let { File(it) }?.safeDelete()
+ field = value
+ }
+
+ fun getCreateRoomConfigFlow(): StateFlow = createRoomConfigFlow
+
+ fun setRoomName(roomName: String) {
+ createRoomConfigFlow.getAndUpdate { config ->
+ val newVisibility = when (config.roomVisibility) {
+ is RoomVisibilityState.Public -> {
+ val roomAddress = config.roomVisibility.roomAddress
+ if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
+ val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
+ config.roomVisibility.copy(
+ roomAddress = RoomAddress.AutoFilled(roomAliasName),
+ )
+ } else {
+ config.roomVisibility
+ }
+ }
+ else -> config.roomVisibility
+ }
+ config.copy(
+ roomName = roomName.takeIf { it.isNotEmpty() },
+ roomVisibility = newVisibility,
+ )
+ }
+ }
+
+ fun setTopic(topic: String) {
+ createRoomConfigFlow.getAndUpdate { config ->
+ config.copy(topic = topic.takeIf { it.isNotEmpty() })
+ }
+ }
+
+ fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
+ cachedAvatarUri = uri.takeIf { cached }
+ createRoomConfigFlow.getAndUpdate { config ->
+ config.copy(avatarUri = uri?.toString())
+ }
+ }
+
+ fun setRoomVisibility(visibility: RoomVisibilityItem) {
+ createRoomConfigFlow.getAndUpdate { config ->
+ config.copy(
+ roomVisibility = when (visibility) {
+ RoomVisibilityItem.Private -> RoomVisibilityState.Private
+ RoomVisibilityItem.Public -> {
+ val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
+ RoomVisibilityState.Public(
+ roomAddress = RoomAddress.AutoFilled(roomAliasName),
+ roomAccess = RoomAccess.Anyone,
+ )
+ }
+ }
+ )
+ }
+ }
+
+ fun setRoomAddress(address: String) {
+ createRoomConfigFlow.getAndUpdate { config ->
+ config.copy(
+ roomVisibility = when (config.roomVisibility) {
+ is RoomVisibilityState.Public -> {
+ val sanitizedAddress = address.lowercase()
+ config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
+ }
+ else -> config.roomVisibility
+ }
+ )
+ }
+ }
+
+ fun setRoomAccess(access: RoomAccessItem) {
+ createRoomConfigFlow.getAndUpdate { config ->
+ config.copy(
+ roomVisibility = when (config.roomVisibility) {
+ is RoomVisibilityState.Public -> {
+ when (access) {
+ RoomAccessItem.Anyone -> config.roomVisibility.copy(roomAccess = RoomAccess.Anyone)
+ RoomAccessItem.AskToJoin -> config.roomVisibility.copy(roomAccess = RoomAccess.Knocking)
+ }
+ }
+ else -> config.roomVisibility
+ }
+ )
+ }
+ }
+
+ fun clearCachedData() {
+ cachedAvatarUri = null
+ }
+}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt
index ef35b654cc9..9d8167cce2e 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccess.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccessItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccessItem.kt
index b3da88bd314..2d37be91036 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccessItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAccessItem.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddress.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddress.kt
index 7e6da517110..a8e4e399994 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddress.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomAddress.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityItem.kt
index 13e0ad7ea73..b92dee4d6e8 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityItem.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt
index 8337a81eae4..82af5a5614d 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomVisibilityState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt
deleted file mode 100644
index b47b8c30e5e..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.di
-
-import com.squareup.anvil.annotations.ContributesTo
-import com.squareup.anvil.annotations.MergeSubcomponent
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
-
-@SingleIn(CreateRoomScope::class)
-@MergeSubcomponent(CreateRoomScope::class)
-interface CreateRoomComponent : NodeFactoriesBindings {
- @MergeSubcomponent.Builder
- interface Builder {
- fun build(): CreateRoomComponent
- }
-
- @ContributesTo(SessionScope::class)
- interface ParentBindings {
- fun createRoomComponentBuilder(): Builder
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt
deleted file mode 100644
index b0a011424ac..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.di
-
-abstract class CreateRoomScope private constructor()
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt
deleted file mode 100644
index d6338ab43c9..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.joinbyaddress
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.libraries.di.SessionScope
-
-@ContributesNode(SessionScope::class)
-class JoinRoomByAddressNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- presenterFactory: JoinRoomByAddressPresenter.Factory,
-) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
- private val presenter = presenterFactory.create(navigator)
-
- @Composable
- override fun View(modifier: Modifier) {
- val state = presenter.present()
- JoinRoomByAddressView(
- state = state,
- modifier = modifier
- )
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt
deleted file mode 100644
index bc3c4a39b91..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import io.element.android.libraries.matrix.api.user.MatrixUser
-
-sealed interface CreateRoomRootEvents {
- data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
- data object CancelStartDM : CreateRoomRootEvents
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt
deleted file mode 100644
index f76a5d5c615..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import android.app.Activity
-import androidx.activity.compose.LocalActivity
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.lifecycle.subscribe
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
-import io.element.android.services.analytics.api.AnalyticsService
-
-@ContributesNode(SessionScope::class)
-class CreateRoomRootNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- private val presenter: CreateRoomRootPresenter,
- private val analyticsService: AnalyticsService,
- private val inviteFriendsUseCase: InviteFriendsUseCase,
-) : Node(buildContext, plugins = plugins) {
- private val navigator = plugins().first()
-
- init {
- lifecycle.subscribe(
- onResume = { analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.StartChat)) }
- )
- }
-
- @Composable
- override fun View(modifier: Modifier) {
- val state = presenter.present()
- val activity = requireNotNull(LocalActivity.current)
- CreateRoomRootView(
- state = state,
- modifier = modifier,
- onCloseClick = this::navigateUp,
- onNewRoomClick = navigator::onCreateNewRoom,
- onOpenDM = {
- navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
- },
- onJoinByAddressClick = navigator::onShowJoinRoomByAddress,
- onInviteFriendsClick = { invitePeople(activity) },
- onRoomDirectorySearchClick = navigator::onOpenRoomDirectory
- )
- }
-
- private fun invitePeople(activity: Activity) {
- inviteFriendsUseCase.execute(activity)
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
deleted file mode 100644
index ea8be45e6b7..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import io.element.android.features.createroom.api.StartDMAction
-import io.element.android.features.createroom.impl.userlist.SelectionMode
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.features.createroom.impl.userlist.UserListPresenter
-import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.featureflag.api.FeatureFlagService
-import io.element.android.libraries.featureflag.api.FeatureFlags
-import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.usersearch.api.UserRepository
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-class CreateRoomRootPresenter @Inject constructor(
- presenterFactory: UserListPresenter.Factory,
- userRepository: UserRepository,
- userListDataStore: UserListDataStore,
- private val startDMAction: StartDMAction,
- private val buildMeta: BuildMeta,
- private val featureFlagService: FeatureFlagService,
-) : Presenter {
- private val presenter = presenterFactory.create(
- UserListPresenterArgs(
- selectionMode = SelectionMode.Single,
- ),
- userRepository,
- userListDataStore,
- )
-
- @Composable
- override fun present(): CreateRoomRootState {
- val userListState = presenter.present()
-
- val localCoroutineScope = rememberCoroutineScope()
- val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
-
- val isRoomDirectorySearchEnabled by remember {
- featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
- }.collectAsState(initial = false)
-
- fun handleEvents(event: CreateRoomRootEvents) {
- when (event) {
- is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
- startDMAction.execute(
- matrixUser = event.matrixUser,
- createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
- actionState = startDmActionState,
- )
- }
- CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
- }
- }
-
- return CreateRoomRootState(
- applicationName = buildMeta.applicationName,
- userListState = userListState,
- startDmAction = startDmActionState.value,
- isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
- eventSink = ::handleEvents,
- )
- }
-}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt
deleted file mode 100644
index 7a6d651db39..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.matrix.api.core.RoomId
-
-data class CreateRoomRootState(
- val applicationName: String,
- val userListState: UserListState,
- val startDmAction: AsyncAction,
- val isRoomDirectorySearchEnabled: Boolean,
- val eventSink: (CreateRoomRootEvents) -> Unit,
-)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
deleted file mode 100644
index 9f2d59e3b6e..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
-import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.ui.components.aMatrixUser
-import io.element.android.libraries.usersearch.api.UserSearchResult
-import kotlinx.collections.immutable.persistentListOf
-
-open class CreateRoomRootStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aCreateRoomRootState(),
- aCreateRoomRootState(
- startDmAction = AsyncAction.Loading,
- userListState = aMatrixUser().let {
- aUserListState().copy(
- searchQuery = it.userId.value,
- searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
- selectedUsers = persistentListOf(it),
- isSearchActive = true,
- )
- }
- ),
- aCreateRoomRootState(
- startDmAction = AsyncAction.Failure(RuntimeException("error")),
- userListState = aMatrixUser().let {
- aUserListState().copy(
- searchQuery = it.userId.value,
- searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
- selectedUsers = persistentListOf(it),
- isSearchActive = true,
- )
- }
- ),
- aCreateRoomRootState(
- userListState = aUserListState(
- recentDirectRooms = aRecentDirectRoomList()
- )
- ),
- aCreateRoomRootState(
- startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()),
- ),
- aCreateRoomRootState(
- isRoomDirectorySearchEnabled = true,
- ),
- )
-}
-
-fun aCreateRoomRootState(
- applicationName: String = "Element X Preview",
- userListState: UserListState = aUserListState(),
- startDmAction: AsyncAction = AsyncAction.Uninitialized,
- isRoomDirectorySearchEnabled: Boolean = false,
- eventSink: (CreateRoomRootEvents) -> Unit = {},
-) = CreateRoomRootState(
- applicationName = applicationName,
- userListState = userListState,
- startDmAction = startDmAction,
- isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
- eventSink = eventSink,
-)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt
deleted file mode 100644
index 248d3319859..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import androidx.annotation.DrawableRes
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.components.UserListView
-import io.element.android.libraries.designsystem.components.async.AsyncActionView
-import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
-import io.element.android.libraries.designsystem.components.button.BackButton
-import io.element.android.libraries.designsystem.icons.CompoundDrawables
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.Icon
-import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
-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.matrix.ui.components.CreateDmConfirmationBottomSheet
-import io.element.android.libraries.matrix.ui.components.MatrixUserRow
-import io.element.android.libraries.ui.strings.CommonStrings
-import kotlinx.collections.immutable.persistentListOf
-
-@Composable
-fun CreateRoomRootView(
- state: CreateRoomRootState,
- onCloseClick: () -> Unit,
- onNewRoomClick: () -> Unit,
- onOpenDM: (RoomId) -> Unit,
- onInviteFriendsClick: () -> Unit,
- onJoinByAddressClick: () -> Unit,
- onRoomDirectorySearchClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- Scaffold(
- modifier = modifier.fillMaxWidth(),
- topBar = {
- if (!state.userListState.isSearchActive) {
- CreateRoomRootViewTopBar(onCloseClick = onCloseClick)
- }
- }
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .padding(paddingValues)
- .consumeWindowInsets(paddingValues),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- UserListView(
- modifier = Modifier.fillMaxWidth(),
- // Do not render suggestions in this case, the suggestion will be rendered
- // by CreateRoomActionButtonsList
- state = state.userListState.copy(
- recentDirectRooms = persistentListOf(),
- ),
- onSelectUser = {
- state.eventSink(CreateRoomRootEvents.StartDM(it))
- },
- onDeselectUser = { },
- )
-
- if (!state.userListState.isSearchActive) {
- CreateRoomActionButtonsList(
- state = state,
- onNewRoomClick = onNewRoomClick,
- onInvitePeopleClick = onInviteFriendsClick,
- onJoinByAddressClick = onJoinByAddressClick,
- onRoomDirectorySearchClick = onRoomDirectorySearchClick,
- onDmClick = onOpenDM,
- )
- }
- }
- }
-
- AsyncActionView(
- async = state.startDmAction,
- progressDialog = {
- AsyncActionViewDefaults.ProgressDialog(
- progressText = stringResource(CommonStrings.common_starting_chat),
- )
- },
- onSuccess = { onOpenDM(it) },
- errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
- onRetry = {
- state.userListState.selectedUsers.firstOrNull()
- ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
- // Cancel start DM if there is no more selected user (should not happen)
- ?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
- },
- onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
- confirmationDialog = { data ->
- if (data is ConfirmingStartDmWithMatrixUser) {
- CreateDmConfirmationBottomSheet(
- matrixUser = data.matrixUser,
- onSendInvite = {
- state.eventSink(CreateRoomRootEvents.StartDM(data.matrixUser))
- },
- onDismiss = {
- state.eventSink(CreateRoomRootEvents.CancelStartDM)
- },
- )
- }
- },
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun CreateRoomRootViewTopBar(
- onCloseClick: () -> Unit,
-) {
- TopAppBar(
- titleStr = stringResource(id = CommonStrings.action_start_chat),
- navigationIcon = {
- BackButton(
- imageVector = CompoundIcons.Close(),
- onClick = onCloseClick,
- )
- }
- )
-}
-
-@Composable
-private fun CreateRoomActionButtonsList(
- state: CreateRoomRootState,
- onNewRoomClick: () -> Unit,
- onInvitePeopleClick: () -> Unit,
- onJoinByAddressClick: () -> Unit,
- onRoomDirectorySearchClick: () -> Unit,
- onDmClick: (RoomId) -> Unit,
-) {
- LazyColumn {
- item {
- CreateRoomActionButton(
- iconRes = CompoundDrawables.ic_compound_plus,
- text = stringResource(id = R.string.screen_create_room_action_create_room),
- onClick = onNewRoomClick,
- )
- }
- if (state.isRoomDirectorySearchEnabled) {
- item {
- CreateRoomActionButton(
- iconRes = CompoundDrawables.ic_compound_list_bulleted,
- text = stringResource(id = R.string.screen_room_directory_search_title),
- onClick = onRoomDirectorySearchClick,
- )
- }
- }
- item {
- CreateRoomActionButton(
- iconRes = CompoundDrawables.ic_compound_share_android,
- text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
- onClick = onInvitePeopleClick,
- )
- }
- item {
- CreateRoomActionButton(
- iconRes = CompoundDrawables.ic_compound_room,
- text = stringResource(R.string.screen_start_chat_join_room_by_address_action),
- onClick = onJoinByAddressClick,
- )
- }
- if (state.userListState.recentDirectRooms.isNotEmpty()) {
- item {
- ListSectionHeader(
- title = stringResource(id = CommonStrings.common_suggestions),
- hasDivider = false,
- )
- }
- state.userListState.recentDirectRooms.forEach { recentDirectRoom ->
- item {
- MatrixUserRow(
- modifier = Modifier.clickable(
- onClick = {
- onDmClick(recentDirectRoom.roomId)
- }
- ),
- matrixUser = recentDirectRoom.matrixUser,
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun CreateRoomActionButton(
- @DrawableRes iconRes: Int,
- text: String,
- onClick: () -> Unit,
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .height(56.dp)
- .clickable { onClick() }
- .padding(horizontal = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- modifier = Modifier.size(24.dp),
- tint = ElementTheme.colors.iconSecondary,
- resourceId = iconRes,
- contentDescription = null,
- )
- Text(
- text = text,
- style = ElementTheme.typography.fontBodyLgRegular,
- )
- }
-}
-
-@PreviewsDayNight
-@Composable
-internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =
- ElementPreview {
- CreateRoomRootView(
- state = state,
- onCloseClick = {},
- onNewRoomClick = {},
- onOpenDM = {},
- onJoinByAddressClick = {},
- onInviteFriendsClick = {},
- onRoomDirectorySearchClick = {},
- )
- }
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt
deleted file mode 100644
index 85cc58e9952..00000000000
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListPresenterArgs.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.userlist
-
-data class UserListPresenterArgs(
- val selectionMode: SelectionMode,
-)
-
-enum class SelectionMode {
- Single,
- Multiple,
-}
diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml
index 8acb990b023..f5d6a234e2d 100644
--- a/features/createroom/impl/src/main/res/values-be/translations.xml
+++ b/features/createroom/impl/src/main/res/values-be/translations.xml
@@ -14,6 +14,4 @@
"Назва пакоя""Стварыце пакой""Тэма (неабавязкова)"
- "Каталог пакояў"
- "Пры спробе пачаць чат адбылася памылка"
diff --git a/features/createroom/impl/src/main/res/values-bg/translations.xml b/features/createroom/impl/src/main/res/values-bg/translations.xml
index fa6dae371c5..249058b7af8 100644
--- a/features/createroom/impl/src/main/res/values-bg/translations.xml
+++ b/features/createroom/impl/src/main/res/values-bg/translations.xml
@@ -15,9 +15,4 @@
"Видимост на стаята""Създаване на стая""Тема за разговор (незадължително)"
- "Присъединяване към стая по адрес"
- "Не е валиден адрес"
- "Въведете…"
- "Стаята не е намерена"
- "напр. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-cs/translations.xml b/features/createroom/impl/src/main/res/values-cs/translations.xml
index 53592e0db0a..e19cfbcf917 100644
--- a/features/createroom/impl/src/main/res/values-cs/translations.xml
+++ b/features/createroom/impl/src/main/res/values-cs/translations.xml
@@ -19,12 +19,4 @@ To můžete kdykoli změnit v nastavení místnosti."
"Viditelnost místnosti""Vytvořit místnost""Téma (nepovinné)"
- "Adresář místností"
- "Při pokusu o zahájení chatu došlo k chybě"
- "Vstoupit do místnosti pomocí adresy"
- "Neplatná adresa"
- "Zadejte…"
- "Odpovídající místnost nalezena"
- "Místnost nebyla nalezena"
- "např. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-cy/translations.xml b/features/createroom/impl/src/main/res/values-cy/translations.xml
index a3a9e30e354..52168014bf8 100644
--- a/features/createroom/impl/src/main/res/values-cy/translations.xml
+++ b/features/createroom/impl/src/main/res/values-cy/translations.xml
@@ -19,12 +19,4 @@ Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."
"Gwelededd yr ystafell""Creu ystafell""Pwnc (dewisol)"
- "Cyfeiriadur ystafelloedd"
- "Digwyddodd gwall wrth geisio cychwyn sgwrs"
- "Ymuno â\'r ystafell yn ôl cyfeiriad"
- "Ddim yn gyfeiriad dilys"
- "Ewch i mewn…"
- "Cafwyd hyd i ystafell gyfatebol"
- "Heb ganfod yr ystafell"
- "e.e. #enw-ystafell:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-da/translations.xml b/features/createroom/impl/src/main/res/values-da/translations.xml
index c9c182b474b..422aae09bd8 100644
--- a/features/createroom/impl/src/main/res/values-da/translations.xml
+++ b/features/createroom/impl/src/main/res/values-da/translations.xml
@@ -1,7 +1,7 @@
"Nyt rum"
- "Invitér folk"
+ "Invitér andre""Der opstod en fejl ved oprettelsen af rummet""Kun inviterede personer kan få adgang til dette rum. Alle meddelelser er ende-til-ende krypteret.""Privat rum"
@@ -14,17 +14,8 @@ Du kan ændre dette når som helst i rummets indstillinger."
"Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen""Spørg om at deltage""Hvis dette rum skal være synligt i det offentlige register, skal du bruge en rum-adresse."
- "Rummets adresse""Navn på rum""Rummets synlighed""Opret et rum""Emne (valgfrit)"
- "Register over rum"
- "Der opstod en fejl under forsøget på at starte en samtale"
- "Tilslut dig rummet med adressen"
- "Ikke en gyldig adresse"
- "Indtast…"
- "Matchende rum fundet"
- "Rum ikke fundet"
- "f.eks. #rummets-navn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml
index 90438aa38c1..9c48001c92b 100644
--- a/features/createroom/impl/src/main/res/values-de/translations.xml
+++ b/features/createroom/impl/src/main/res/values-de/translations.xml
@@ -1,30 +1,22 @@
- "Neuer Raum"
+ "Neuer Chat""Nutzer einladen""Beim Erstellen des Chats ist ein Fehler aufgetreten"
- "Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind Ende-zu-Ende verschlüsselt."
- "Privater Chatroom"
- "Alle können diesen Chatroom finden.
-Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."
- "Öffentlicher Raum"
- "Jeder darf diesen Raum betreten"
+ "Nur eingeladene Personen haben Zutritt zu diesem Chat. Alle Nachrichten sind Ende-zu-Ende verschlüsselt."
+ "Privater Chat"
+ "Jeder kann diesen Chat finden.
+Du kannst dies jederzeit in den Einstellungen des Chats ändern."
+ "Öffentlicher Chat"
+ "Jeder darf diesem Chat beitreten""Jeder"
- "Chatroomzugang"
- "Jeder kann den Zutritt zum Raum beantragen, aber ein Moderator muss die Anfrage akzeptieren."
+ "Chat Zugang"
+ "Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren.""Beitritt beantragen"
- "Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."
- "Chatroomadresse"
- "Raumname"
- " Sichtbarkeit des Chatrooms"
- "Raum erstellen"
+ "Du benötigst eine Chat-Adresse, damit dieser Chat im öffentlichen Verzeichnis sichtbar ist."
+ "Chatroom Adresse"
+ "Chat-Name"
+ " Sichtbarkeit des Chats"
+ "Chat erstellen""Thema (optional)"
- "Raum-Verzeichnis"
- "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"
- "Raum per Adresse betreten"
- "Keine gültige Adresse"
- "Eintreten…"
- "Passender Raum gefunden"
- "Raum nicht gefunden"
- "z. B. #room -name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-el/translations.xml b/features/createroom/impl/src/main/res/values-el/translations.xml
index c036b43a05e..37ccd49d0ed 100644
--- a/features/createroom/impl/src/main/res/values-el/translations.xml
+++ b/features/createroom/impl/src/main/res/values-el/translations.xml
@@ -14,17 +14,9 @@
"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή ένας συντονιστής θα πρέπει να αποδεχτεί το αίτημα""Αίτημα συμμετοχής""Για να είναι ορατή αυτή η αίθουσα στον δημόσιο κατάλογο αιθουσών, θα χρειαστείτε μια διεύθυνση αίθουσας."
- "Διεύθυνση αίθουσας"
+ "Διεύθυνση δωματίου""Όνομα αίθουσας""Ορατότητα αίθουσας""Δημιουργία αίθουσας""Θέμα (προαιρετικό)"
- "Κατάλογος αιθουσών"
- "Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"
- "Συμμετοχή σε αίθουσα μέσω διεύθυνσης"
- "Μη έγκυρη διεύθυνση"
- "Εισάγετε…"
- "Βρέθηκε η αντίστοιχη αίθουσα"
- "Η αίθουσα δεν βρέθηκε"
- "π.χ. #όνομα-αίθουσας:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml
index c11f08fc141..64806977c03 100644
--- a/features/createroom/impl/src/main/res/values-es/translations.xml
+++ b/features/createroom/impl/src/main/res/values-es/translations.xml
@@ -14,17 +14,8 @@ Puedes cambiar esto en cualquier momento en los ajustes de la sala."
"Cualquiera puede solicitar unirse a la sala, pero un administrador o un moderador tendrá que aceptar la solicitud""Solicitud para unirse""Para que esta sala sea visible en el directorio de salas públicas, necesitarás una dirección de sala."
- "Dirección de la sala""Nombre de la sala""Visibilidad de la sala""Crear una sala""Tema (opcional)"
- "Directorio de salas"
- "Se ha producido un error al intentar iniciar un chat"
- "Unirse a una sala por su dirección"
- "Dirección no válida"
- "Introducir…"
- "Sala encontrada"
- "No se encontró la sala"
- "p. ej., #nombre-de-la-sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-et/translations.xml b/features/createroom/impl/src/main/res/values-et/translations.xml
index 57a26106b3f..6a1d9dc58ae 100644
--- a/features/createroom/impl/src/main/res/values-et/translations.xml
+++ b/features/createroom/impl/src/main/res/values-et/translations.xml
@@ -19,12 +19,4 @@ Sa võid seda jututoa seadistustest alati muuta."
"Jututoa nähtavus""Loo jututuba""Teema (kui soovid lisada)"
- "Jututubade kataloog"
- "Vestluse alustamisel tekkis viga"
- "Liitu jututoaga aadressi alusel"
- "See pole kehtiv aadress"
- "Sisene…"
- "Leidsime vastava jututoa"
- "Jututuba ei leidu"
- "nt. #jututoa-nimi:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-eu/translations.xml b/features/createroom/impl/src/main/res/values-eu/translations.xml
index 43f67e429c1..537aa495a5b 100644
--- a/features/createroom/impl/src/main/res/values-eu/translations.xml
+++ b/features/createroom/impl/src/main/res/values-eu/translations.xml
@@ -16,8 +16,4 @@ Gelaren ezarpenetan aldatu dezakezu hobespena."
"Gelaren ikusgarritasuna""Sortu gela""Mintzagaia (aukerakoa)"
- "Gelen direktorioa"
- "Errorea gertatu da txata hasten saiatzean"
- "Sartu…"
- "Ez da gela aurkitu"
diff --git a/features/createroom/impl/src/main/res/values-fa/translations.xml b/features/createroom/impl/src/main/res/values-fa/translations.xml
index 03bc5c3f58e..09869c76f6c 100644
--- a/features/createroom/impl/src/main/res/values-fa/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fa/translations.xml
@@ -17,12 +17,4 @@
"نمایانی اتاق""ایجاد اتاق""موضوع (اختیاری)"
- "فهرست اتاقها"
- "هنگام تلاش برای شروع چت خطایی روی داد"
- "پیوستن به اتاق با نشانی"
- "نشانی معتبری نیست"
- "ورود…"
- "اتاق مطابق پیدا شد"
- "اتاق پیدا نشد"
- "نمونه: #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-fi/translations.xml b/features/createroom/impl/src/main/res/values-fi/translations.xml
index cdb6d04d598..31589ab9971 100644
--- a/features/createroom/impl/src/main/res/values-fi/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fi/translations.xml
@@ -19,12 +19,4 @@ Voit muuttaa tämän milloin tahansa huoneen asetuksista."
"Huoneen näkyvyys""Luo huone""Aihe (valinnainen)"
- "Huoneluettelo"
- "Keskustelun aloituksessa tapahtui virhe"
- "Liity huoneeseen osoitteella"
- "Osoite ei ole kelvollinen"
- "Syötä…"
- "Täsmäävä huone löytyi"
- "Huonetta ei löytynyt"
- "esim. #huoneen-nimi:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-fr/translations.xml b/features/createroom/impl/src/main/res/values-fr/translations.xml
index 3f9e01dc85a..afbdc919ba7 100644
--- a/features/createroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/createroom/impl/src/main/res/values-fr/translations.xml
@@ -19,12 +19,4 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon.""Visibilité du salon"
"Créer un salon""Sujet (facultatif)"
- "Annuaire des salons"
- "Une erreur s’est produite lors de la tentative de création de la discussion"
- "Saisir une adresse de salon"
- "Ce n’est pas une adresse valide"
- "Saisir…"
- "Ce salon existe"
- "Salon non trouvé"
- "ex: #nom-du-salon:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-hu/translations.xml b/features/createroom/impl/src/main/res/values-hu/translations.xml
index 1e4a7212793..24f71983ce2 100644
--- a/features/createroom/impl/src/main/res/values-hu/translations.xml
+++ b/features/createroom/impl/src/main/res/values-hu/translations.xml
@@ -14,17 +14,9 @@ Ezt bármikor módosíthatja a szobabeállításokban."
"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést""Csatlakozás kérése""Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."
- "A szoba címe"
+ "Szoba címe""Szoba neve""Szoba láthatósága""Szoba létrehozása""Téma (nem kötelező)"
- "Szobakatalógus"
- "Hiba történt a csevegés indításakor"
- "Csatlakozás a szobához cím szerint"
- "Nem érvényes cím"
- "Írja be…"
- "Megfelelő szoba található"
- "Szoba nem található"
- "pl. #szoba-neve:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-in/translations.xml b/features/createroom/impl/src/main/res/values-in/translations.xml
index 65f135e617e..219f621068f 100644
--- a/features/createroom/impl/src/main/res/values-in/translations.xml
+++ b/features/createroom/impl/src/main/res/values-in/translations.xml
@@ -19,12 +19,4 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."
"Keterlihatan ruangan""Buat ruangan""Topik (opsional)"
- "Direktori ruangan"
- "Terjadi kesalahan saat mencoba memulai obrolan"
- "Bergabung dalam ruangan berdasarkan alamat"
- "Bukan alamat yang valid"
- "Masuk…"
- "Ruangan yang cocok ditemukan"
- "Ruangan tidak ditemukan"
- "mis. #nama-ruangan:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml
index 701ebc967f0..741b88b7635 100644
--- a/features/createroom/impl/src/main/res/values-it/translations.xml
+++ b/features/createroom/impl/src/main/res/values-it/translations.xml
@@ -19,12 +19,4 @@ Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."
"Visibilità della stanza""Crea una stanza""Argomento (facoltativo)"
- "Elenco delle stanze"
- "Si è verificato un errore durante il tentativo di avviare una chat"
- "Accedi alla stanza tramite indirizzo"
- "Indirizzo non valido"
- "Inserisci…"
- "Stanza trovata"
- "Stanza non trovata"
- "ad esempio #room -name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ka/translations.xml b/features/createroom/impl/src/main/res/values-ka/translations.xml
index bb4a4303209..20c7af40de1 100644
--- a/features/createroom/impl/src/main/res/values-ka/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ka/translations.xml
@@ -10,6 +10,4 @@
"ოთახის სახელი""ოთახის შექმნა""თემა (სურვილისამებრ)"
- "ოთახის კატალოგი"
- "ჩატის დაწყების მცდელობისას შეცდომა მოხდა"
diff --git a/features/createroom/impl/src/main/res/values-ko/translations.xml b/features/createroom/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..3dfdc46e7cd
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,21 @@
+
+
+ "새 방"
+ "사람 초대하기"
+ "방을 생성하던 중 오류가 발생했어요"
+ "초대받은 사람만 이 방에 액세스할 수 있습니다. 모든 메시지는 종단 간 암호화됩니다."
+ "비공개 방"
+ "누구나 이 방을 찾을 수 있습니다.
+방 설정에서 언제든지 변경할 수 있습니다."
+ "공개 방"
+ "누구나 이 방에 참여할 수 있습니다."
+ "누구나"
+ "방 액세스"
+ "누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다."
+ "참가 요청"
+ "이 방이 공개 방 디렉토리에 표시되려면 방 주소가 필요합니다."
+ "방 이름"
+ "방 표시 여부"
+ "방 만들기"
+ "주제 (선택)"
+
diff --git a/features/createroom/impl/src/main/res/values-lt/translations.xml b/features/createroom/impl/src/main/res/values-lt/translations.xml
index 2f7a4438ea7..2fb7b3eb5c4 100644
--- a/features/createroom/impl/src/main/res/values-lt/translations.xml
+++ b/features/createroom/impl/src/main/res/values-lt/translations.xml
@@ -10,5 +10,4 @@ Tai galite bet kada pakeisti kambario nustatymuose."
"Kambario pavadinimas""Kurti kambarį""Tema (nebūtina)"
- "Bandant pradėti pokalbį įvyko klaida"
diff --git a/features/createroom/impl/src/main/res/values-nb/translations.xml b/features/createroom/impl/src/main/res/values-nb/translations.xml
index 9266993fd4e..9a159811124 100644
--- a/features/createroom/impl/src/main/res/values-nb/translations.xml
+++ b/features/createroom/impl/src/main/res/values-nb/translations.xml
@@ -14,17 +14,8 @@ Du kan endre dette når som helst i rominnstillingene."
"Alle kan be om å få bli med i rommet, men en administrator eller moderator må godta forespørselen""Be om å bli med""For at dette rommet skal være synlig i den offentlige romkatalogen, trenger du en romadresse."
- "Romadresse""Romnavn""Romsynlighet""Opprett et rom""Emne (valgfritt)"
- "Romkatalog"
- "Det oppstod en feil når du prøvde å starte en chat"
- "Bli med i rommet med adresse"
- "Ikke en gyldig adresse"
- "Gå inn…"
- "Matchende rom funnet"
- "Rom ikke funnet"
- "f.eks. #rom-navn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-nl/translations.xml b/features/createroom/impl/src/main/res/values-nl/translations.xml
index 29d070a2638..5142148171f 100644
--- a/features/createroom/impl/src/main/res/values-nl/translations.xml
+++ b/features/createroom/impl/src/main/res/values-nl/translations.xml
@@ -16,6 +16,4 @@ Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."
"Naam van de kamer""Creëer een kamer""Onderwerp (optioneel)"
- "Kamergids"
- "Er is een fout opgetreden bij het starten van een chat"
diff --git a/features/createroom/impl/src/main/res/values-pl/translations.xml b/features/createroom/impl/src/main/res/values-pl/translations.xml
index 3ed917b7bb9..446644b6222 100644
--- a/features/createroom/impl/src/main/res/values-pl/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pl/translations.xml
@@ -19,12 +19,4 @@ Możesz to zmienić w ustawieniach pokoju."
"Widoczność pomieszczenia""Utwórz pokój""Temat (opcjonalnie)"
- "Katalog pokoi"
- "Wystąpił błąd podczas próby rozpoczęcia czatu"
- "Dołącz do pokoju za pomocą adresu"
- "Nieprawidłowy adres"
- "Wprowadź…"
- "Znaleziono pasujący pokój"
- "Nie znaleziono pokoju"
- "np. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
index 52c9f569af5..73aaaa503e2 100644
--- a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml
@@ -3,7 +3,7 @@
"Nova sala""Convidar pessoas""Ocorreu um erro ao criar a sala"
- "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são criptografadas de ponta a ponta."
+ "Apenas as pessoas convidadas podem entrar nesta sala. Todas as mensagens são criptografadas de ponta a ponta.""Sala privada""Qualquer um pode encontrar esta sala.
Você pode mudar isso a qualquer momento nas configurações da sala."
@@ -14,17 +14,8 @@ Você pode mudar isso a qualquer momento nas configurações da sala."
"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação""Pedir para entrar""Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala."
- "Endereço da sala""Nome da sala""Visibilidade da sala""Criar uma sala""Tópico (opcional)"
- "Diretório de salas"
- "Ocorreu um erro ao tentar iniciar um chat"
- "Entrar na sala pelo endereço"
- "Não é um endereço válido"
- "Entrar…"
- "Foi encontrada uma sala correspondente"
- "Sala não encontrada"
- "Por exemplo, #nome-da-sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-pt/translations.xml b/features/createroom/impl/src/main/res/values-pt/translations.xml
index 5e5f5fc9c3f..1524914bb2a 100644
--- a/features/createroom/impl/src/main/res/values-pt/translations.xml
+++ b/features/createroom/impl/src/main/res/values-pt/translations.xml
@@ -19,12 +19,4 @@ Pode alterar esta opção nas definições da sala."
"Visibilidade da sala""Criar uma sala""Descrição (opcional)"
- "Diretório de salas"
- "Ocorreu um erro ao tentar iniciar uma conversa"
- "Entrar na sala pelo endereço"
- "Não é um endereço válido"
- "Entrar…"
- "Sala correspondente encontrado"
- "Sala não encontrada"
- "por exemplo, #sala:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml
index 8d9e74b5307..b9fe78bb198 100644
--- a/features/createroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ro/translations.xml
@@ -11,12 +11,12 @@ Puteți modifica acest lucru oricând în setări."
"Oricine se poate alătura acestei camere""Oricine""Acces la cameră"
- "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte solicitarea"
+ "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea""Cereți să vă alăturați""Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."
+ "Adresa camerei""Numele camerei"
+ "Vizibilitatea camerei""Creați o cameră""Subiect (opțional)"
- "Director de camere"
- "A apărut o eroare la încercarea începerii conversației"
diff --git a/features/createroom/impl/src/main/res/values-ru/translations.xml b/features/createroom/impl/src/main/res/values-ru/translations.xml
index 6344491248c..e8716731148 100644
--- a/features/createroom/impl/src/main/res/values-ru/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ru/translations.xml
@@ -19,12 +19,4 @@
"Видимость комнаты""Создать комнату""Тема (необязательно)"
- "Каталог комнат"
- "Произошла ошибка при запуске чата"
- "Присоединиться к комнате по адресу"
- "Недействительный адрес"
- "Ввести…"
- "Соответствующая комната найдена"
- "Комната не найдена"
- "прим. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-sk/translations.xml b/features/createroom/impl/src/main/res/values-sk/translations.xml
index eaf6c2f19c3..7b6d89b2e1d 100644
--- a/features/createroom/impl/src/main/res/values-sk/translations.xml
+++ b/features/createroom/impl/src/main/res/values-sk/translations.xml
@@ -19,12 +19,4 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."
"Viditeľnosť miestnosti""Vytvoriť miestnosť""Téma (voliteľné)"
- "Adresár miestností"
- "Pri pokuse o spustenie konverzácie sa vyskytla chyba"
- "Pripojte sa do miestnosti podľa adresy"
- "Neplatná adresa"
- "Zadajte…"
- "Nájdená zodpovedajúca miestnosť"
- "Miestnosť sa nenašla"
- "napr. #nazov-miestnosti:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-sv/translations.xml b/features/createroom/impl/src/main/res/values-sv/translations.xml
index 316c9cc32e4..8cd01ebd0ef 100644
--- a/features/createroom/impl/src/main/res/values-sv/translations.xml
+++ b/features/createroom/impl/src/main/res/values-sv/translations.xml
@@ -19,12 +19,4 @@ Du kan ändra detta när som helst i rumsinställningarna."
"Rumssynlighet""Skapa ett rum""Ämne (valfritt)"
- "Rumskatalog"
- "Ett fel uppstod när du försökte starta en chatt"
- "Gå med i rum med adress"
- "Inte en giltig adress"
- "Ange …"
- "Matchande rum hittades"
- "Rummet hittades inte"
- "t.ex. #rumsnamn:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-tr/translations.xml b/features/createroom/impl/src/main/res/values-tr/translations.xml
index c8e31365171..d97139c973e 100644
--- a/features/createroom/impl/src/main/res/values-tr/translations.xml
+++ b/features/createroom/impl/src/main/res/values-tr/translations.xml
@@ -19,6 +19,4 @@ Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."
"Oda görünürlüğü""Bir oda oluştur""Konu (isteğe bağlı)"
- "Oda dizini"
- "Sohbet başlatmaya çalışırken bir hata oluştu"
diff --git a/features/createroom/impl/src/main/res/values-uk/translations.xml b/features/createroom/impl/src/main/res/values-uk/translations.xml
index a43172fb8b5..047b4dd9d24 100644
--- a/features/createroom/impl/src/main/res/values-uk/translations.xml
+++ b/features/createroom/impl/src/main/res/values-uk/translations.xml
@@ -19,12 +19,4 @@
"Видимість кімнати""Створити кімнату""Тема (необов\'язково)"
- "Каталог кімнат"
- "Під час спроби почати бесіду сталася помилка"
- "Приєднатися до кімнати за адресою"
- "Недійсна адреса"
- "Введіть…"
- "Знайдено відповідну кімнату"
- "Кімната не знайдена"
- "наприклад, #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-ur/translations.xml b/features/createroom/impl/src/main/res/values-ur/translations.xml
index 6fe476462ed..b68992085fe 100644
--- a/features/createroom/impl/src/main/res/values-ur/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ur/translations.xml
@@ -11,6 +11,4 @@
"کمرے کا نام""ایک کمرہ بنائیں""موضوع (اختیاری)"
- "کمرے کا راہنامچہ"
- "گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"
diff --git a/features/createroom/impl/src/main/res/values-uz/translations.xml b/features/createroom/impl/src/main/res/values-uz/translations.xml
index e3bc9de5288..34062f96699 100644
--- a/features/createroom/impl/src/main/res/values-uz/translations.xml
+++ b/features/createroom/impl/src/main/res/values-uz/translations.xml
@@ -3,11 +3,19 @@
"Yangi xona""Odamlarni taklif qiling""Xonani yaratishda xatolik yuz berdi"
- "Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni o‘chirib bo‘lmaydi."
- "Shaxsiy xona (faqat taklif)"
- "Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."
+ "Faqat taklif etilgan shaxslargina bu xonaga kira oladi. Barcha xabarlar boshdan-oxirigacha shifrlanadi."
+ "Shaxsiy xona"
+ "Bu xonani har kim topishi mumkin.
+Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin."
+ "Jamoat xonasi"
+ "Bu xonaga istalgan kishi qo‘shilishi mumkin"
+ "Har kim"
+ "Xonaga kirish"
+ "Xonaga qo‘shilishni istalgan kishi so‘rashi mumkin, lekin administrator yoki moderator so‘rovni qabul qilishi kerak"
+ "Qo‘shilishni so‘rang"
+ "Ushbu xona ommaviy xonalar ro‘yxatida ko‘rinishi uchun sizga xona manzili kerak bo‘ladi.""Xona nomi"
+ "Xonaning ko‘rinishi""Xonani yaratish""Mavzu (ixtiyoriy)"
- "Suhbatni boshlashda xatolik yuz berdi"
diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
index 0d67c883c7b..476f9cff7eb 100644
--- a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
@@ -19,12 +19,4 @@
"聊天室能見度""建立聊天室""主題(非必填)"
- "聊天室目錄"
- "嘗試開始聊天時發生錯誤"
- "按地址加入聊天室"
- "不是有效的位址"
- "輸入……"
- "找到相符的聊天室"
- "找不到聊天室"
- "例如 #room-name:matrix.org"
diff --git a/features/createroom/impl/src/main/res/values-zh/translations.xml b/features/createroom/impl/src/main/res/values-zh/translations.xml
index 24d7df7f353..d20e34801d0 100644
--- a/features/createroom/impl/src/main/res/values-zh/translations.xml
+++ b/features/createroom/impl/src/main/res/values-zh/translations.xml
@@ -19,6 +19,4 @@
"房间可见性""创建聊天室""主题(可选)"
- "聊天室目录"
- "在开始聊天时发生了错误"
diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml
index d011a1d87b0..fa9a1cb2760 100644
--- a/features/createroom/impl/src/main/res/values/localazy.xml
+++ b/features/createroom/impl/src/main/res/values/localazy.xml
@@ -19,12 +19,4 @@ You can change this anytime in room settings."
"Room visibility""Create a room""Topic (optional)"
- "Room directory"
- "An error occurred when trying to start a chat"
- "Join room by address"
- "Not a valid address"
- "Enter…"
- "Matching room found"
- "Room not found"
- "e.g. #room-name:matrix.org"
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt
new file mode 100644
index 00000000000..35b6637bbf6
--- /dev/null
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.createroom.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.createroom.api.CreateRoomEntryPoint
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultCreateRoomEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultCreateRoomEntryPoint()
+
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ CreateRoomFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ )
+ }
+ val callback = object : CreateRoomEntryPoint.Callback {
+ override fun onRoomCreated(roomId: RoomId) = lambdaError()
+ }
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ callback = callback,
+ )
+ assertThat(result.plugins).contains(callback)
+ }
+}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt
deleted file mode 100644
index 28dcdd409d6..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl
-
-import io.element.android.features.createroom.CreateRoomNavigator
-import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-
-class FakeCreateRoomNavigator(
- private val openRoomLambda: (roomIdOrAlias: RoomIdOrAlias, serverNames: List) -> Unit = { _, _ -> },
- private val createNewRoomLambda: () -> Unit = {},
- private val showJoinRoomByAddressLambda: () -> Unit = {},
- private val dismissJoinRoomByAddressLambda: () -> Unit = {},
- private val openRoomDirectoryLambda: () -> Unit = {},
-) : CreateRoomNavigator {
- override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) {
- openRoomLambda(roomIdOrAlias, serverNames)
- }
-
- override fun onCreateNewRoom() {
- createNewRoomLambda()
- }
-
- override fun onShowJoinRoomByAddress() {
- showJoinRoomByAddressLambda()
- }
-
- override fun onDismissJoinRoomByAddress() {
- dismissJoinRoomByAddressLambda()
- }
-
- override fun onOpenRoomDirectory() {
- openRoomDirectoryLambda()
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt
deleted file mode 100644
index acb381dadcd..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-import app.cash.molecule.RecompositionMode
-import app.cash.molecule.moleculeFlow
-import app.cash.turbine.test
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
-import io.element.android.libraries.usersearch.test.FakeUserRepository
-import io.element.android.tests.testutils.WarmUpRule
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class AddPeoplePresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- private lateinit var presenter: AddPeoplePresenter
-
- @Before
- fun setup() {
- presenter = AddPeoplePresenter(
- FakeUserListPresenterFactory(),
- FakeUserRepository(),
- CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
- )
- }
-
- @Test
- fun `present - initial state`() = runTest {
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- // TODO This doesn't actually test anything...
- val initialState = awaitItem()
- assertThat(initialState)
- }
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt
deleted file mode 100644
index c18abf0daae..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.addpeople
-
-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.features.createroom.impl.userlist.UserListEvents
-import io.element.android.features.createroom.impl.userlist.UserListState
-import io.element.android.features.createroom.impl.userlist.aUserListState
-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 AddPeopleViewTest {
- @get:Rule
- val rule = createAndroidComposeRule()
-
- @Test
- fun `clicking on back invokes the expected callback`() {
- val eventsRecorder = EventsRecorder()
- ensureCalledOnce {
- rule.setAddPeopleView(
- aUserListState(
- eventSink = eventsRecorder,
- ),
- onBackClick = it
- )
- rule.pressBack()
- }
- eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
- }
-
- @Test
- fun `clicking on back during search emits the expected Event`() {
- val eventsRecorder = EventsRecorder()
- rule.setAddPeopleView(
- aUserListState(
- isSearchActive = true,
- eventSink = eventsRecorder,
- ),
- )
- rule.pressBack()
- eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false))
- }
-
- @Test
- fun `clicking on skip invokes the expected callback`() {
- val eventsRecorder = EventsRecorder()
- ensureCalledOnce {
- rule.setAddPeopleView(
- aUserListState(
- eventSink = eventsRecorder,
- ),
- onNextClick = it
- )
- rule.clickOn(CommonStrings.action_skip)
- }
- eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
- }
-}
-
-private fun AndroidComposeTestRule.setAddPeopleView(
- state: UserListState,
- onBackClick: () -> Unit = EnsureNeverCalled(),
- onNextClick: () -> Unit = EnsureNeverCalled(),
-) {
- setContent {
- AddPeopleView(
- state = state,
- onBackClick = onBackClick,
- onNextClick = onNextClick,
- )
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
deleted file mode 100644
index a0a4783093b..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.configureroom
-
-import android.net.Uri
-import app.cash.turbine.TurbineTestContext
-import com.google.common.truth.Truth.assertThat
-import im.vector.app.features.analytics.plan.CreatedRoom
-import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.featureflag.api.FeatureFlags
-import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
-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.alias.ResolvedRoomAlias
-import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
-import io.element.android.libraries.matrix.test.AN_AVATAR_URL
-import io.element.android.libraries.matrix.test.AN_EXCEPTION
-import io.element.android.libraries.matrix.test.A_MESSAGE
-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.FakeMatrixClient
-import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
-import io.element.android.libraries.matrix.ui.components.aMatrixUser
-import io.element.android.libraries.matrix.ui.media.AvatarAction
-import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
-import io.element.android.libraries.mediapickers.api.PickerProvider
-import io.element.android.libraries.mediapickers.test.FakePickerProvider
-import io.element.android.libraries.mediaupload.api.MediaPreProcessor
-import io.element.android.libraries.mediaupload.api.MediaUploadInfo
-import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
-import io.element.android.libraries.permissions.api.PermissionsPresenter
-import io.element.android.libraries.permissions.test.FakePermissionsPresenter
-import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
-import io.element.android.services.analytics.api.AnalyticsService
-import io.element.android.services.analytics.test.FakeAnalyticsService
-import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.test
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.mockkStatic
-import io.mockk.unmockkAll
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import java.io.File
-import java.util.Optional
-
-private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
-private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
-private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
-
-@RunWith(RobolectricTestRunner::class)
-class ConfigureBaseRoomPresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- @Before
- fun setup() {
- mockkStatic(File::readBytes)
- every { any().readBytes() } returns byteArrayOf()
- }
-
- @After
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun `present - initial state`() = runTest {
- val presenter = createConfigureRoomPresenter()
- presenter.test {
- val initialState = initialState()
- assertThat(initialState.config).isEqualTo(CreateRoomConfig())
- assertThat(initialState.config.roomName).isNull()
- assertThat(initialState.config.topic).isNull()
- assertThat(initialState.config.invites).isEmpty()
- assertThat(initialState.config.avatarUri).isNull()
- assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
- assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- assertThat(initialState.homeserverName).isEqualTo("matrix.org")
- }
- }
-
- @Test
- fun `present - create room button is enabled only if the required fields are completed`() = runTest {
- val presenter = createConfigureRoomPresenter()
- presenter.test {
- val initialState = initialState()
- var config = initialState.config
- assertThat(initialState.isValid).isFalse()
-
- // Room name not empty
- initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
- var newState: ConfigureRoomState = awaitItem()
- config = config.copy(roomName = A_ROOM_NAME)
- assertThat(newState.config).isEqualTo(config)
- assertThat(newState.isValid).isTrue()
-
- // Clear room name
- newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
- newState = awaitItem()
- config = config.copy(roomName = null)
- assertThat(newState.config).isEqualTo(config)
- assertThat(newState.isValid).isFalse()
- }
- }
-
- @Test
- fun `present - state is updated when fields are changed`() = runTest {
- val userListDataStore = UserListDataStore()
- val pickerProvider = FakePickerProvider()
- val permissionsPresenter = FakePermissionsPresenter()
- val roomAliasHelper = FakeRoomAliasHelper()
- val presenter = createConfigureRoomPresenter(
- createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
- pickerProvider = pickerProvider,
- permissionsPresenter = permissionsPresenter,
- )
- presenter.test {
- val initialState = initialState()
- var expectedConfig = CreateRoomConfig()
- assertThat(initialState.config).isEqualTo(expectedConfig)
-
- // Select User
- val selectedUser1 = aMatrixUser()
- val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob")
- userListDataStore.selectUser(selectedUser1)
- skipItems(1)
- userListDataStore.selectUser(selectedUser2)
- var newState = awaitItem()
- expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2))
- assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Room name
- initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
- assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Room topic
- newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
- assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Room avatar
- // Pick avatar
- pickerProvider.givenResult(null)
- // From gallery
- val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
- pickerProvider.givenResult(uriFromGallery)
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
- assertThat(newState.config).isEqualTo(expectedConfig)
- // From camera
- val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
- pickerProvider.givenResult(uriFromCamera)
- assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
- newState = awaitItem()
- assertThat(newState.cameraPermissionState.showDialog).isTrue()
- permissionsPresenter.setPermissionGranted()
- newState = awaitItem()
- assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
- assertThat(newState.config).isEqualTo(expectedConfig)
- // Do it again, no permission is requested
- val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
- pickerProvider.givenResult(uriFromCamera2)
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
- assertThat(newState.config).isEqualTo(expectedConfig)
- // Remove
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(avatarUri = null)
- assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Room privacy
- newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(
- roomVisibility = RoomVisibilityState.Public(
- roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
- roomAccess = RoomAccess.Anyone,
- )
- )
- assertThat(newState.config).isEqualTo(expectedConfig)
-
- // Remove user
- newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
- newState = awaitItem()
- expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
- assertThat(newState.config).isEqualTo(expectedConfig)
- }
- }
-
- @Test
- fun `present - trigger create room action`() = runTest {
- val matrixClient = createMatrixClient()
- val presenter = createConfigureRoomPresenter(
- matrixClient = matrixClient
- )
- presenter.test {
- val initialState = initialState()
- val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
-
- matrixClient.givenCreateRoomResult(createRoomResult)
-
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
- val stateAfterCreateRoom = awaitItem()
- assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
- assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
- }
- }
-
- @Test
- fun `present - record analytics when creating room`() = runTest {
- val matrixClient = createMatrixClient()
- val analyticsService = FakeAnalyticsService()
- val presenter = createConfigureRoomPresenter(
- matrixClient = matrixClient,
- analyticsService = analyticsService
- )
- presenter.test {
- val initialState = initialState()
- val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
-
- matrixClient.givenCreateRoomResult(createRoomResult)
-
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
- skipItems(2)
-
- val analyticsEvent = analyticsService.capturedEvents.filterIsInstance().firstOrNull()
- assertThat(analyticsEvent).isNotNull()
- assertThat(analyticsEvent?.isDM).isFalse()
- }
- }
-
- @Test
- fun `present - trigger create room with upload error and retry`() = runTest {
- val matrixClient = createMatrixClient()
- val analyticsService = FakeAnalyticsService()
- val mediaPreProcessor = FakeMediaPreProcessor()
- val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
- val presenter = createConfigureRoomPresenter(
- createRoomDataStore = createRoomDataStore,
- mediaPreProcessor = mediaPreProcessor,
- matrixClient = matrixClient,
- analyticsService = analyticsService
- )
- presenter.test {
- val initialState = initialState()
- createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
- skipItems(1)
- mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
- matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
-
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
- val stateAfterCreateRoom = awaitItem()
- assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
- assertThat(analyticsService.capturedEvents.filterIsInstance()).isEmpty()
-
- matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
- stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
- }
- }
-
- @Test
- fun `present - trigger retry and cancel actions`() = runTest {
- val fakeMatrixClient = createMatrixClient()
- val presenter = createConfigureRoomPresenter(
- matrixClient = fakeMatrixClient
- )
- presenter.test {
- val initialState = initialState()
- val createRoomResult = Result.failure(AN_EXCEPTION)
-
- fakeMatrixClient.givenCreateRoomResult(createRoomResult)
-
- // Create
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
- val stateAfterCreateRoom = awaitItem()
- assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
- assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
-
- // Retry
- stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
- val stateAfterRetry = awaitItem()
- assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
- assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
-
- // Cancel
- stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
- assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun `present - address is invalid when format is invalid`() = runTest {
- val aliasHelper = FakeRoomAliasHelper(
- isRoomAliasValidLambda = { false }
- )
- val presenter = createConfigureRoomPresenter(
- roomAliasHelper = aliasHelper
- )
- presenter.test {
- val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
- skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
- skipItems(1)
- advanceUntilIdle()
- awaitItem().also { state ->
- assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
- }
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun `present - address is not available when alias is not available`() = runTest {
- val fakeMatrixClient = createMatrixClient(isAliasAvailable = false)
- val presenter = createConfigureRoomPresenter(
- matrixClient = fakeMatrixClient,
- )
- presenter.test {
- val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
- skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
- skipItems(1)
- advanceUntilIdle()
- awaitItem().also { state ->
- assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
- }
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun `present - address is valid when alias is available and format is valid`() = runTest {
- val fakeMatrixClient = createMatrixClient(isAliasAvailable = true)
- val presenter = createConfigureRoomPresenter(
- matrixClient = fakeMatrixClient,
- )
- presenter.test {
- val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
- skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
- skipItems(1)
- advanceUntilIdle()
- awaitItem().also { state ->
- assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
- }
- }
- }
-
- private suspend fun TurbineTestContext.initialState(): ConfigureRoomState {
- skipItems(1)
- return awaitItem()
- }
-
- private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
- userIdServerNameLambda = { "matrix.org" },
- resolveRoomAliasResult = {
- val resolvedRoomAlias = if (isAliasAvailable) {
- Optional.empty()
- } else {
- Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
- }
- Result.success(resolvedRoomAlias)
- }
- )
-
- private fun createConfigureRoomPresenter(
- roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
- createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
- matrixClient: MatrixClient = createMatrixClient(),
- pickerProvider: PickerProvider = FakePickerProvider(),
- mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
- analyticsService: AnalyticsService = FakeAnalyticsService(),
- permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
- isKnockFeatureEnabled: Boolean = true,
- ) = ConfigureRoomPresenter(
- dataStore = createRoomDataStore,
- matrixClient = matrixClient,
- mediaPickerProvider = pickerProvider,
- mediaPreProcessor = mediaPreProcessor,
- analyticsService = analyticsService,
- permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
- roomAliasHelper = roomAliasHelper,
- featureFlagService = FakeFeatureFlagService(
- mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
- )
- )
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
deleted file mode 100644
index d2d9959e2a6..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import androidx.compose.runtime.MutableState
-import app.cash.molecule.RecompositionMode
-import app.cash.molecule.moleculeFlow
-import app.cash.turbine.test
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
-import io.element.android.features.createroom.api.StartDMAction
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
-import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
-import io.element.android.features.createroom.impl.userlist.UserListDataStore
-import io.element.android.features.createroom.test.FakeStartDMAction
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.featureflag.api.FeatureFlags
-import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
-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.user.MatrixUser
-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.core.aBuildMeta
-import io.element.android.libraries.usersearch.test.FakeUserRepository
-import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.lambda.any
-import io.element.android.tests.testutils.lambda.lambdaRecorder
-import io.element.android.tests.testutils.lambda.value
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-
-class CreateBaseRoomRootPresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- @Test
- fun `present - start DM action failure scenario`() = runTest {
- val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION)
- val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
- actionState.value = startDMFailureResult
- }
- val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
- assertThat(initialState.userListState.selectedUsers).isEmpty()
- assertThat(initialState.userListState.isSearchActive).isFalse()
- assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
- val matrixUser = MatrixUser(UserId("@name:domain"))
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
- awaitItem().also { state ->
- assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
- executeResult.assertions().isCalledOnce().with(
- value(matrixUser),
- value(false),
- any(),
- )
- state.eventSink(CreateRoomRootEvents.CancelStartDM)
- }
- awaitItem().also { state ->
- assertThat(state.startDmAction.isUninitialized()).isTrue()
- }
- }
- }
-
- @Test
- fun `present - start DM action success scenario`() = runTest {
- val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
- val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
- actionState.value = startDMSuccessResult
- }
- val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
- assertThat(initialState.userListState.selectedUsers).isEmpty()
- assertThat(initialState.userListState.isSearchActive).isFalse()
- assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
- val matrixUser = MatrixUser(UserId("@name:domain"))
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
- awaitItem().also { state ->
- assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
- executeResult.assertions().isCalledOnce().with(
- value(matrixUser),
- value(false),
- any(),
- )
- }
- }
- }
-
- @Test
- fun `present - start DM action confirmation scenario - cancel`() = runTest {
- val matrixUser = MatrixUser(UserId("@name:domain"))
- val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
- val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
- actionState.value = startDMConfirmationResult
- }
- val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
- val confirmingState = awaitItem()
- assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
- executeResult.assertions().isCalledOnce().with(
- value(matrixUser),
- value(false),
- any(),
- )
- // Cancelling should not create the DM
- confirmingState.eventSink(CreateRoomRootEvents.CancelStartDM)
- val finalState = awaitItem()
- assertThat(finalState.startDmAction.isUninitialized()).isTrue()
- executeResult.assertions().isCalledExactly(1)
- }
- }
-
- @Test
- fun `present - start DM action confirmation scenario - confirm`() = runTest {
- val matrixUser = MatrixUser(UserId("@name:domain"))
- val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
- val executeResult = lambdaRecorder>, Unit> { _, _, actionState ->
- actionState.value = startDMConfirmationResult
- }
- val startDMAction = FakeStartDMAction(executeResult = executeResult)
- val presenter = createCreateRoomRootPresenter(startDMAction)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
- initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
- val confirmingState = awaitItem()
- assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
- executeResult.assertions().isCalledOnce().with(
- value(matrixUser),
- value(false),
- any(),
- )
- // Start DM again should invoke the action with createIfDmDoesNotExist = true
- confirmingState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
- executeResult.assertions().isCalledExactly(2).withSequence(
- listOf(value(matrixUser), value(false), any()),
- listOf(value(matrixUser), value(true), any()),
- )
- }
- }
-
- @Test
- fun `present - room directory search`() = runTest {
- val presenter = createCreateRoomRootPresenter(isRoomDirectorySearchEnabled = true)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- awaitItem().let { state ->
- assertThat(state.isRoomDirectorySearchEnabled).isTrue()
- }
- }
- }
-
- private fun createCreateRoomRootPresenter(
- startDMAction: StartDMAction = FakeStartDMAction(),
- isRoomDirectorySearchEnabled: Boolean = false,
- ): CreateRoomRootPresenter {
- val featureFlagService = FakeFeatureFlagService(
- initialState = mapOf(
- FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled,
- ),
- )
- return CreateRoomRootPresenter(
- presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()),
- userRepository = FakeUserRepository(),
- userListDataStore = UserListDataStore(),
- startDMAction = startDMAction,
- featureFlagService = featureFlagService,
- buildMeta = aBuildMeta(),
- )
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt
deleted file mode 100644
index 9104b2dfb2e..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.root
-
-import androidx.activity.ComponentActivity
-import androidx.compose.ui.test.junit4.AndroidComposeTestRule
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
-import io.element.android.features.createroom.impl.userlist.aUserListState
-import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.ui.model.getBestName
-import io.element.android.libraries.ui.strings.CommonStrings
-import io.element.android.tests.testutils.EnsureNeverCalled
-import io.element.android.tests.testutils.EnsureNeverCalledWithParam
-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.ensureCalledOnceWithParam
-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
-import org.robolectric.annotation.Config
-
-@RunWith(AndroidJUnit4::class)
-class CreateBaseRoomRootViewTest {
- @get:Rule
- val rule = createAndroidComposeRule()
-
- @Test
- fun `clicking on back invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- eventSink = eventsRecorder,
- ),
- onCloseClick = it
- )
- rule.pressBack()
- }
- }
-
- @Test
- fun `clicking on New room invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- eventSink = eventsRecorder,
- ),
- onNewRoomClick = it
- )
- rule.clickOn(R.string.screen_create_room_action_create_room)
- }
- }
-
- @Config(qualifiers = "h1024dp")
- @Test
- fun `clicking on Invite people invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- applicationName = "test",
- eventSink = eventsRecorder,
- ),
- onInviteFriendsClick = it
- )
- val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test")
- rule.onNodeWithText(text).performClick()
- }
- }
-
- @Config(qualifiers = "h1024dp")
- @Test
- fun `clicking on a user suggestion invokes the expected callback`() {
- val recentDirectRoomList = aRecentDirectRoomList()
- val firstRoom = recentDirectRoomList[0]
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnceWithParam(firstRoom.roomId) {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- userListState = aUserListState(
- recentDirectRooms = recentDirectRoomList
- ),
- eventSink = eventsRecorder,
- ),
- onOpenDM = it
- )
- rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick()
- }
- }
-
- @Config(qualifiers = "h1024dp")
- @Test
- fun `clicking on Join room by address invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- eventSink = eventsRecorder,
- ),
- onJoinRoomByAddressClick = it
- )
- rule.clickOn(R.string.screen_start_chat_join_room_by_address_action)
- }
- }
-
- @Test
- fun `clicking on room directory invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setCreateRoomRootView(
- aCreateRoomRootState(
- eventSink = eventsRecorder,
- isRoomDirectorySearchEnabled = true
- ),
- onRoomDirectorySearchClick = it
- )
- rule.clickOn(R.string.screen_room_directory_search_title)
- }
- }
-}
-
-private fun AndroidComposeTestRule.setCreateRoomRootView(
- state: CreateRoomRootState,
- onCloseClick: () -> Unit = EnsureNeverCalled(),
- onNewRoomClick: () -> Unit = EnsureNeverCalled(),
- onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
- onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
- onJoinRoomByAddressClick: () -> Unit = EnsureNeverCalled(),
- onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
-) {
- setContent {
- CreateRoomRootView(
- state = state,
- onCloseClick = onCloseClick,
- onNewRoomClick = onNewRoomClick,
- onOpenDM = onOpenDM,
- onInviteFriendsClick = onInviteFriendsClick,
- onJoinByAddressClick = onJoinRoomByAddressClick,
- onRoomDirectorySearchClick = onRoomDirectorySearchClick,
- )
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt
deleted file mode 100644
index a0768ffda84..00000000000
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/FakeUserListPresenter.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.createroom.impl.userlist
-
-import androidx.compose.runtime.Composable
-
-class FakeUserListPresenter : UserListPresenter {
- private var state = aUserListState()
-
- fun givenState(state: UserListState) {
- this.state = state
- }
-
- @Composable
- override fun present(): UserListState {
- return state
- }
-}
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt
new file mode 100644
index 00000000000..c8a6c2bd8a9
--- /dev/null
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/startchat/impl/configureroom/ConfigureRoomPresenterTest.kt
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.startchat.impl.configureroom
+
+import android.net.Uri
+import androidx.core.net.toUri
+import app.cash.turbine.TurbineTestContext
+import com.google.common.truth.Truth.assertThat
+import im.vector.app.features.analytics.plan.CreatedRoom
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
+import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
+import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
+import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
+import io.element.android.features.createroom.impl.configureroom.RoomAccess
+import io.element.android.features.createroom.impl.configureroom.RoomAddress
+import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
+import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
+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.alias.ResolvedRoomAlias
+import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.matrix.test.A_MESSAGE
+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.FakeMatrixClient
+import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
+import io.element.android.libraries.matrix.ui.media.AvatarAction
+import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
+import io.element.android.libraries.mediapickers.api.PickerProvider
+import io.element.android.libraries.mediapickers.test.FakePickerProvider
+import io.element.android.libraries.mediaupload.api.MediaPreProcessor
+import io.element.android.libraries.mediaupload.api.MediaUploadInfo
+import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
+import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.element.android.libraries.permissions.api.PermissionsPresenter
+import io.element.android.libraries.permissions.test.FakePermissionsPresenter
+import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
+import io.element.android.services.analytics.api.AnalyticsService
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.test
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import java.io.File
+import java.util.Optional
+
+private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
+private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
+private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
+
+@RunWith(RobolectricTestRunner::class)
+class ConfigureRoomPresenterTest {
+ @get:Rule
+ val warmUpRule = WarmUpRule()
+
+ @Before
+ fun setup() {
+ mockkStatic(File::readBytes)
+ every { any().readBytes() } returns byteArrayOf()
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createConfigureRoomPresenter()
+ presenter.test {
+ val initialState = initialState()
+ assertThat(initialState.config).isEqualTo(CreateRoomConfig())
+ assertThat(initialState.config.roomName).isNull()
+ assertThat(initialState.config.topic).isNull()
+ assertThat(initialState.config.invites).isEmpty()
+ assertThat(initialState.config.avatarUri).isNull()
+ assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
+ assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(initialState.homeserverName).isEqualTo("matrix.org")
+ }
+ }
+
+ @Test
+ fun `present - create room button is enabled only if the required fields are completed`() = runTest {
+ val presenter = createConfigureRoomPresenter()
+ presenter.test {
+ val initialState = initialState()
+ var config = initialState.config
+ assertThat(initialState.isValid).isFalse()
+
+ // Room name not empty
+ initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
+ var newState: ConfigureRoomState = awaitItem()
+ config = config.copy(roomName = A_ROOM_NAME)
+ assertThat(newState.config).isEqualTo(config)
+ assertThat(newState.isValid).isTrue()
+
+ // Clear room name
+ newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
+ newState = awaitItem()
+ config = config.copy(roomName = null)
+ assertThat(newState.config).isEqualTo(config)
+ assertThat(newState.isValid).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - state is updated when fields are changed`() = runTest {
+ val pickerProvider = FakePickerProvider()
+ val permissionsPresenter = FakePermissionsPresenter()
+ val roomAliasHelper = FakeRoomAliasHelper()
+ val presenter = createConfigureRoomPresenter(
+ dataStore = CreateRoomConfigStore(roomAliasHelper),
+ pickerProvider = pickerProvider,
+ permissionsPresenter = permissionsPresenter,
+ )
+ presenter.test {
+ val initialState = initialState()
+ var expectedConfig = CreateRoomConfig()
+ assertThat(initialState.config).isEqualTo(expectedConfig)
+ // Room name
+ initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
+ var newState = awaitItem()
+ expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+
+ // Room topic
+ newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+
+ // Room avatar
+ // Pick avatar
+ pickerProvider.givenResult(null)
+ // From gallery
+ val uriFromGallery = AN_URI_FROM_GALLERY
+ pickerProvider.givenResult(uriFromGallery.toUri())
+ newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+ // From camera
+ val uriFromCamera = AN_URI_FROM_CAMERA
+ pickerProvider.givenResult(uriFromCamera.toUri())
+ assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
+ newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
+ newState = awaitItem()
+ assertThat(newState.cameraPermissionState.showDialog).isTrue()
+ permissionsPresenter.setPermissionGranted()
+ newState = awaitItem()
+ assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+ // Do it again, no permission is requested
+ val uriFromCamera2 = AN_URI_FROM_CAMERA_2
+ pickerProvider.givenResult(uriFromCamera2.toUri())
+ newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+ // Remove
+ newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(avatarUri = null)
+ assertThat(newState.config).isEqualTo(expectedConfig)
+
+ // Room privacy
+ newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
+ newState = awaitItem()
+ expectedConfig = expectedConfig.copy(
+ roomVisibility = RoomVisibilityState.Public(
+ roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
+ roomAccess = RoomAccess.Anyone,
+ )
+ )
+ assertThat(newState.config).isEqualTo(expectedConfig)
+ }
+ }
+
+ @Test
+ fun `present - trigger create room action`() = runTest {
+ val matrixClient = createMatrixClient()
+ val presenter = createConfigureRoomPresenter(
+ matrixClient = matrixClient
+ )
+ presenter.test {
+ val initialState = initialState()
+ val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
+
+ matrixClient.givenCreateRoomResult(createRoomResult)
+
+ initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
+ val stateAfterCreateRoom = awaitItem()
+ assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
+ assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
+ }
+ }
+
+ @Test
+ fun `present - record analytics when creating room`() = runTest {
+ val matrixClient = createMatrixClient()
+ val analyticsService = FakeAnalyticsService()
+ val presenter = createConfigureRoomPresenter(
+ matrixClient = matrixClient,
+ analyticsService = analyticsService
+ )
+ presenter.test {
+ val initialState = initialState()
+ val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
+
+ matrixClient.givenCreateRoomResult(createRoomResult)
+
+ initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ skipItems(2)
+
+ val analyticsEvent = analyticsService.capturedEvents.filterIsInstance().firstOrNull()
+ assertThat(analyticsEvent).isNotNull()
+ assertThat(analyticsEvent?.isDM).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - trigger create room with upload error and retry`() = runTest {
+ val matrixClient = createMatrixClient()
+ val analyticsService = FakeAnalyticsService()
+ val mediaPreProcessor = FakeMediaPreProcessor()
+ val dataStore = CreateRoomConfigStore(FakeRoomAliasHelper())
+ val presenter = createConfigureRoomPresenter(
+ dataStore = dataStore,
+ mediaPreProcessor = mediaPreProcessor,
+ matrixClient = matrixClient,
+ analyticsService = analyticsService
+ )
+ presenter.test {
+ val initialState = initialState()
+ dataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
+ skipItems(1)
+ mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
+ matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
+
+ initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
+ val stateAfterCreateRoom = awaitItem()
+ assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
+ assertThat(analyticsService.capturedEvents.filterIsInstance()).isEmpty()
+
+ matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
+ stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
+ }
+ }
+
+ @Test
+ fun `present - trigger retry and cancel actions`() = runTest {
+ val fakeMatrixClient = createMatrixClient()
+ val presenter = createConfigureRoomPresenter(
+ matrixClient = fakeMatrixClient
+ )
+ presenter.test {
+ val initialState = initialState()
+ val createRoomResult = Result.failure(AN_EXCEPTION)
+
+ fakeMatrixClient.givenCreateRoomResult(createRoomResult)
+
+ // Create
+ initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
+ val stateAfterCreateRoom = awaitItem()
+ assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
+ assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
+
+ // Retry
+ stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
+ val stateAfterRetry = awaitItem()
+ assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
+ assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
+
+ // Cancel
+ stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
+ assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `present - address is invalid when format is invalid`() = runTest {
+ val aliasHelper = FakeRoomAliasHelper(
+ isRoomAliasValidLambda = { false }
+ )
+ val presenter = createConfigureRoomPresenter(
+ roomAliasHelper = aliasHelper
+ )
+ presenter.test {
+ val initialState = initialState()
+ initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
+ skipItems(1)
+ initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
+ skipItems(1)
+ advanceUntilIdle()
+ awaitItem().also { state ->
+ assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `present - address is not available when alias is not available`() = runTest {
+ val fakeMatrixClient = createMatrixClient(isAliasAvailable = false)
+ val presenter = createConfigureRoomPresenter(
+ matrixClient = fakeMatrixClient,
+ )
+ presenter.test {
+ val initialState = initialState()
+ initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
+ skipItems(1)
+ initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
+ skipItems(1)
+ advanceUntilIdle()
+ awaitItem().also { state ->
+ assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `present - address is valid when alias is available and format is valid`() = runTest {
+ val fakeMatrixClient = createMatrixClient(isAliasAvailable = true)
+ val presenter = createConfigureRoomPresenter(
+ matrixClient = fakeMatrixClient,
+ )
+ presenter.test {
+ val initialState = initialState()
+ initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
+ skipItems(1)
+ initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
+ skipItems(1)
+ advanceUntilIdle()
+ awaitItem().also { state ->
+ assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
+ }
+ }
+ }
+
+ private suspend fun TurbineTestContext.initialState(): ConfigureRoomState {
+ skipItems(1)
+ return awaitItem()
+ }
+
+ private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
+ userIdServerNameLambda = { "matrix.org" },
+ resolveRoomAliasResult = {
+ val resolvedRoomAlias = if (isAliasAvailable) {
+ Optional.empty()
+ } else {
+ Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
+ }
+ Result.success(resolvedRoomAlias)
+ }
+ )
+
+ private fun createConfigureRoomPresenter(
+ roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
+ dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
+ matrixClient: MatrixClient = createMatrixClient(),
+ pickerProvider: PickerProvider = FakePickerProvider(),
+ mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
+ analyticsService: AnalyticsService = FakeAnalyticsService(),
+ permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
+ isKnockFeatureEnabled: Boolean = true,
+ mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
+ ) = ConfigureRoomPresenter(
+ dataStore = dataStore,
+ matrixClient = matrixClient,
+ mediaPickerProvider = pickerProvider,
+ mediaPreProcessor = mediaPreProcessor,
+ analyticsService = analyticsService,
+ permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
+ roomAliasHelper = roomAliasHelper,
+ featureFlagService = FakeFeatureFlagService(
+ mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
+ ),
+ mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
+ )
+}
diff --git a/features/createroom/test/build.gradle.kts b/features/createroom/test/build.gradle.kts
index b7df0caab66..98aeac8a4c3 100644
--- a/features/createroom/test/build.gradle.kts
+++ b/features/createroom/test/build.gradle.kts
@@ -1,12 +1,12 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-
plugins {
- id("io.element.android-compose-library")
+ id("io.element.android-library")
}
android {
@@ -14,10 +14,7 @@ android {
}
dependencies {
- implementation(libs.coroutines.core)
- implementation(projects.libraries.matrix.api)
- implementation(projects.libraries.matrix.test)
+ implementation(projects.features.createroom.api)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
- api(projects.features.createroom.api)
}
diff --git a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt
new file mode 100644
index 00000000000..2beaecf0135
--- /dev/null
+++ b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.createroom.api
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ callback: CreateRoomEntryPoint.Callback,
+ ): Node = lambdaError()
+}
diff --git a/features/deactivation/api/build.gradle.kts b/features/deactivation/api/build.gradle.kts
index 58a73813ea6..64a9f250718 100644
--- a/features/deactivation/api/build.gradle.kts
+++ b/features/deactivation/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt b/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt
index a6bf42e03a8..6694d1ecb4c 100644
--- a/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt
+++ b/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/build.gradle.kts b/features/deactivation/impl/build.gradle.kts
index 7694ac8bc17..bca144071b1 100644
--- a/features/deactivation/impl/build.gradle.kts
+++ b/features/deactivation/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,7 +24,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
implementation(projects.libraries.androidutils)
@@ -34,14 +36,6 @@ dependencies {
implementation(projects.libraries.uiStrings)
api(projects.features.deactivation.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+ testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt
index 755f4a7b1f8..5ceb97b232a 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt
index 59c45c5bb59..44be7b5d533 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,13 +13,14 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class AccountDeactivationNode @AssistedInject constructor(
+@AssistedInject
+class AccountDeactivationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: AccountDeactivationPresenter,
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
index d8e8137d2d9..eaeacd022ce 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,15 +13,16 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class AccountDeactivationPresenter @Inject constructor(
+@Inject
+class AccountDeactivationPresenter(
private val matrixClient: MatrixClient,
) : Presenter {
@Composable
@@ -32,7 +34,7 @@ class AccountDeactivationPresenter @Inject constructor(
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
- fun handleEvents(event: AccountDeactivationEvents) {
+ fun handleEvent(event: AccountDeactivationEvents) {
when (event) {
is AccountDeactivationEvents.SetEraseData -> {
updateFormState(formState) {
@@ -62,7 +64,7 @@ class AccountDeactivationPresenter @Inject constructor(
return AccountDeactivationState(
deactivateFormState = formState.value,
accountDeactivationAction = action.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
index 2ef2db318bb..ca9751efccf 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt
index 12b9a7d5546..5c832a4d75e 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
index af1bcdad8df..c0d625a45e5 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt
index 5481db19d7e..c93d519e679 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,14 +10,13 @@ package io.element.android.features.logout.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultAccountDeactivationEntryPoint @Inject constructor() : AccountDeactivationEntryPoint {
+class DefaultAccountDeactivationEntryPoint : AccountDeactivationEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode(buildContext)
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt
index 4e927cd07d7..84037d7eb1c 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt
index b3dfc931d4b..905112a78df 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/main/res/values-da/translations.xml b/features/deactivation/impl/src/main/res/values-da/translations.xml
index e9d5d9fddae..c6dcb1710a0 100644
--- a/features/deactivation/impl/src/main/res/values-da/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-da/translations.xml
@@ -6,8 +6,8 @@
"Deaktivering af din konto er %1$s, det vil:""irreversibel""%1$s din konto (du kan ikke logge ind igen, og dit ID kan ikke genbruges)."
- "Deaktiver permanent"
- "Fjern dig fra alle samtalerum"
+ "Permanent deaktivere"
+ "Fjerne dig fra alle samtaler""Slette dine kontooplysninger fra vores identitetsserver.""Dine beskeder vil stadig være synlige for registrerede brugere, men vil ikke være tilgængelige for nye eller uregistrerede brugere, hvis du vælger at slette dem.""Deaktiver konto"
diff --git a/features/deactivation/impl/src/main/res/values-de/translations.xml b/features/deactivation/impl/src/main/res/values-de/translations.xml
index cd61a6a9d00..1aec7495a1e 100644
--- a/features/deactivation/impl/src/main/res/values-de/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-de/translations.xml
@@ -1,14 +1,14 @@
- "Bitte bestätigen Sie, dass Sie Ihr Benutzerkonto deaktivieren möchten. Diese Aktion kann nicht rückgängig gemacht werden."
+ "Bitte bestätige, dass du dein Konto deaktivieren möchtest. Dies kann nicht rückgängig gemacht werden.""Lösche alle meine Nachrichten"
- "Warnung: Benutzern werden möglicherweise unvollständige Konversationen angezeigt."
- "Wenn Sie Ihr Konto deaktivieren%1$s, wird es:"
+ "Warnung: Künftigen Nutzern werden möglicherweise unvollständige Konversationen angezeigt."
+ "Dein Konto zu deaktivieren ist %1$s. Folgendes wird passieren:""irreversibel"
- "%1$s Ihr Konto (Sie können sich nicht erneut anmelden und Ihre ID kann nicht wiederverwendet werden)."
+ "%1$s dein Konto (du kannst dich nicht erneut anmelden und deine ID kann nicht wiederverwendet werden).""Dauerhaft deaktivieren"
- "Sie werden aus allen Chatrooms entfernt."
- "Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."
- "Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."
+ "Du wirst aus allen Chats entfernt."
+ "Lösche deine Kontoinformationen von unserem Identitätsserver."
+ "Deine Nachrichten werden für bereits registrierte Nutzer weiterhin sichtbar sein. Für neue oder unregistrierte Nutzer sind sie nicht verfügbar, wenn du sie löschen solltest.""Nutzerkonto deaktivieren"
diff --git a/features/deactivation/impl/src/main/res/values-eu/translations.xml b/features/deactivation/impl/src/main/res/values-eu/translations.xml
index d55c272e656..3df431cd8ca 100644
--- a/features/deactivation/impl/src/main/res/values-eu/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-eu/translations.xml
@@ -1,9 +1,12 @@
+ "Baieztatu zure kontua desaktibatu nahi duzula. Ekintza hau ezin da desegin.""Ezabatu nire mezu guztiak""Kontuaren desaktibazioa %1$s, honakoa eragingo du:""ezin da desegin""Ezgaitu betiko""Kendu zure burua txat gela guztietatik."
+ "Ezabatu zure kontuaren informazioa gure identitate-zerbitzaritik."
+ "Zure mezuak erregistratutako erabiltzaileentzat ikusgai egongo dira oraindik, baina ezabatzen badituzu, ez dira eskuragarri egongo erabiltzaile berri edo erregistratu gabeentzat.""Desaktibatu kontua"
diff --git a/features/deactivation/impl/src/main/res/values-hu/translations.xml b/features/deactivation/impl/src/main/res/values-hu/translations.xml
index 47651f0ff93..3d3722b8efb 100644
--- a/features/deactivation/impl/src/main/res/values-hu/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-hu/translations.xml
@@ -8,7 +8,7 @@
"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra).""Véglegesen letiltja""Eltávolításra kerül az összes csevegőszobából."
- "Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."
+ "Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról.""Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket.""Fiók deaktiválása"
diff --git a/features/deactivation/impl/src/main/res/values-ko/translations.xml b/features/deactivation/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..6b7953a4a54
--- /dev/null
+++ b/features/deactivation/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "계정을 비활성화하시겠습니까? 이 작업은 되돌릴 수 없습니다."
+ "모든 내 메시지 삭제"
+ "경고: 향후 사용자는 불완전한 대화 내용을 볼 수 있습니다."
+ "계정을 비활성화하는 것은 %1$s 이며, 다음과 같은 조치를 취합니다:"
+ "불가역적"
+ "%1$s 귀하의 계정 (로그인할 수 없으며, 귀하의 ID는 재사용할 수 없습니다)."
+ "영구적으로 비활성화"
+ "모든 채팅방에서 자신을 제거하세요."
+ "당사의 신원 서버에서 귀하의 계정 정보를 삭제하세요."
+ "메시지는 등록된 사용자에게는 계속 표시되지만, 삭제하면 신규 또는 미등록 사용자는 볼 수 없게 됩니다."
+ "계정 비활성화"
+
diff --git a/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml b/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml
index 51a412a7a3a..7000a65d476 100644
--- a/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml
@@ -1,14 +1,14 @@
"Confirme que você deseja desativar sua conta. Essa ação não pode ser desfeita."
- "Excluir todas as minhas mensagens"
- "Aviso: Os futuros usuários poderão ver conversas incompletas."
+ "Apagar todas as minhas mensagens"
+ "Alerta: Usuários futuros podem ver conversas incompletas.""Desativar sua conta é %1$s, isso irá:""irreversível"
- "%1$s sua conta (você não poderá fazer login novamente, e seu ID não poderá ser reutilizado)."
- "Desativar permanentemente"
+ "%1$s (você não poderá entrar novamente, e seu ID não poderá ser reutilizado)."
+ "Desativar a sua conta permanentemente""Te remover de todas as salas de conversa."
- "Exclua as informações da sua conta do nosso servidor de identidade."
- "Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."
+ "Apague as informações da sua conta do nosso servidor de identidade."
+ "Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por apagá-las.""Desativar conta"
diff --git a/features/deactivation/impl/src/main/res/values-pt/translations.xml b/features/deactivation/impl/src/main/res/values-pt/translations.xml
index 536f4cec348..0a8c618e440 100644
--- a/features/deactivation/impl/src/main/res/values-pt/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-pt/translations.xml
@@ -1,6 +1,6 @@
- "Confirme que pretende desativar a sua conta. Esta ação não pode ser desfeita."
+ "Confirma que pretendes desativar a tua conta. Esta ação não pode ser desfeita.""Eliminar todas as minhas mensagens""Aviso: futuros usuários podem ver conversas incompletas.""A desativação da sua conta é %1$s, irá:"
diff --git a/features/deactivation/impl/src/main/res/values-ro/translations.xml b/features/deactivation/impl/src/main/res/values-ro/translations.xml
index 2fe403ec236..acd4c0747d8 100644
--- a/features/deactivation/impl/src/main/res/values-ro/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-ro/translations.xml
@@ -7,7 +7,7 @@
"ireversibilă""%1$s contul dumneavoastră (nu vă puteți conecta din nou, iar ID-ul dvs. nu poate fi reutilizat).""Dezactivați permanent"
- "Elimina din toate camerele de chat."
+ "Îndepărta din toate camerele de chat.""Șterge informațiile contului dumneavoastră de pe serverul nostru de identitate.""Mesajele dumneavoastră vor fi în continuare vizibile pentru utilizatorii înregistrați, dar nu vor fi disponibile pentru utilizatorii noi sau neînregistrați dacă alegeți să le ștergeți.""Dezactivați contul"
diff --git a/features/deactivation/impl/src/main/res/values-ur/translations.xml b/features/deactivation/impl/src/main/res/values-ur/translations.xml
new file mode 100644
index 00000000000..297b29c519f
--- /dev/null
+++ b/features/deactivation/impl/src/main/res/values-ur/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "براہ کرم تصدیق کریں کہ آپ اپنا اکاؤنٹ غیر فعال کرنا چاہتے ہیں۔ اس کارروائی کو کالعدم نہیں کیا جا سکتا۔"
+ "میرے تمام پیغامات ڈیلیٹ کریں۔"
+ "انتباہ: مستقبل کے صارفین نامکمل گفتگو دیکھ سکتے ہیں۔"
+ "اپنے اکاؤنٹ کو غیر فعال کرنا %1$s ہے، یہ کرے گا:"
+ "ناقابل واپسی"
+ "%1$s آپ کا اکاؤنٹ (آپ دوبارہ لاگ ان نہیں ہو سکتے، اور آپ کی ID کو دوبارہ استعمال نہیں کیا جا سکتا)۔"
+ "مستقل طور پر غیر فعال کریں"
+ "آپ کو تمام چیت رومز سے ہٹا دے گا۔"
+ "ہمارے شناختی سرور سے اپنے اکاؤنٹ کی معلومات کو حذف کریں۔"
+ "آپ کے پیغامات اب بھی رجسٹرڈ صارفین کو نظر آئیں گے لیکن اگر آپ انہیں حذف کرنے کا انتخاب کرتے ہیں تو نئے یا غیر رجسٹرڈ صارفین کے لیے دستیاب نہیں ہوں گے۔"
+ "اکاؤنٹ کو غیر فعال کریں"
+
diff --git a/features/deactivation/impl/src/main/res/values-uz/translations.xml b/features/deactivation/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 00000000000..19a70bb1499
--- /dev/null
+++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi."
+ "Barcha xabarlarimni o‘chirib tashlang"
+ "Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."
+ "Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"
+ "qaytarilmas"
+ "%1$s hisobingiz (qaytadan kirolmaysiz va ID qayta ishlatilmaydi)."
+ "Butunlay faolsizlantirish"
+ "Sizni barcha chat xonalaridan olib tashlash."
+ "Hisobingiz haqidagi axborotni identifikatsiya serverimizdan o‘chirib tashlang."
+ "Xabarlaringiz ro‘yxatdan o‘tgan foydalanuvchilarga ko‘rinadi, lekin ularni o‘chirishni tanlasangiz, yangi yoki ro‘yxatdan o‘tmagan foydalanuvchilarga ko‘rinmaydi."
+ "Hisobni faolsizlantirish"
+
diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
index 1383bdde21f..ee7f8e470d1 100644
--- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
+++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -148,10 +149,10 @@ class AccountDeactivationPresenterTest {
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
}
}
-
- private fun createPresenter(
- matrixClient: MatrixClient = FakeMatrixClient(),
- ) = AccountDeactivationPresenter(
- matrixClient = matrixClient,
- )
}
+
+internal fun createPresenter(
+ matrixClient: MatrixClient = FakeMatrixClient(),
+) = AccountDeactivationPresenter(
+ matrixClient = matrixClient,
+)
diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
index 1da7568aaec..eff479d21c7 100644
--- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
+++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.kt
new file mode 100644
index 00000000000..a9fdf3ff364
--- /dev/null
+++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPointTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.logout.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultAccountDeactivationEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultAccountDeactivationEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ AccountDeactivationNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenter = createPresenter(),
+ )
+ }
+ val result = entryPoint.createNode(parentNode, BuildContext.root(null))
+ assertThat(result).isInstanceOf(AccountDeactivationNode::class.java)
+ }
+}
diff --git a/features/deactivation/test/build.gradle.kts b/features/deactivation/test/build.gradle.kts
new file mode 100644
index 00000000000..e1050d7aaad
--- /dev/null
+++ b/features/deactivation/test/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.deactivation.test"
+}
+
+dependencies {
+ implementation(projects.features.deactivation.api)
+ implementation(projects.libraries.architecture)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/deactivation/test/src/main/kotlin/io/element/android/features/deactivation/test/FakeAccountDeactivationEntryPoint.kt b/features/deactivation/test/src/main/kotlin/io/element/android/features/deactivation/test/FakeAccountDeactivationEntryPoint.kt
new file mode 100644
index 00000000000..ada24833f6d
--- /dev/null
+++ b/features/deactivation/test/src/main/kotlin/io/element/android/features/deactivation/test/FakeAccountDeactivationEntryPoint.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.deactivation.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeAccountDeactivationEntryPoint : AccountDeactivationEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ ): Node {
+ lambdaError()
+ }
+}
diff --git a/features/enterprise/api/build.gradle.kts b/features/enterprise/api/build.gradle.kts
index 9f63ab2cf1f..1c541c5080b 100644
--- a/features/enterprise/api/build.gradle.kts
+++ b/features/enterprise/api/build.gradle.kts
@@ -1,11 +1,12 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
- id("io.element.android-library")
+ id("io.element.android-compose-library")
}
android {
@@ -13,7 +14,7 @@ android {
}
dependencies {
- implementation(libs.compound)
+ implementation(projects.libraries.compound)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt
new file mode 100644
index 00000000000..86ba110da7d
--- /dev/null
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.enterprise.api
+
+sealed interface BugReportUrl {
+ data object UseDefault : BugReportUrl
+ data object Disabled : BugReportUrl
+ data class Custom(
+ val url: String,
+ ) : BugReportUrl
+}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
index 4e2a3cc1f2f..ecf1ba6b64a 100644
--- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
@@ -1,14 +1,17 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api
-import io.element.android.compound.tokens.generated.SemanticColors
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.core.SessionId
+import kotlinx.coroutines.flow.Flow
interface EnterpriseService {
val isEnterpriseBuild: Boolean
@@ -16,12 +19,22 @@ interface EnterpriseService {
fun defaultHomeserverList(): List
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
- fun semanticColorsLight(): SemanticColors
- fun semanticColorsDark(): SemanticColors
+ /**
+ * Override the brand color.
+ * @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session.
+ * @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default.
+ */
+ suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
+
+ fun brandColorsFlow(sessionId: SessionId?): Flow
+
+ fun semanticColorsFlow(sessionId: SessionId?): Flow
fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String?
+ fun bugReportUrlFlow(sessionId: SessionId?): Flow
+
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"
}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt
index c67b94476be..6bd6c78de52 100644
--- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,4 +10,6 @@ package io.element.android.features.enterprise.api
interface SessionEnterpriseService {
suspend fun isElementCallAvailable(): Boolean
+
+ suspend fun init()
}
diff --git a/features/enterprise/impl-foss/build.gradle.kts b/features/enterprise/impl-foss/build.gradle.kts
new file mode 100644
index 00000000000..f0c63f56551
--- /dev/null
+++ b/features/enterprise/impl-foss/build.gradle.kts
@@ -0,0 +1,29 @@
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
+
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.enterprise.impl"
+}
+
+setupDependencyInjection()
+
+dependencies {
+ implementation(projects.libraries.compound)
+ api(projects.features.enterprise.api)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+
+ testCommonDependencies(libs)
+ testImplementation(projects.libraries.matrix.test)
+}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
new file mode 100644
index 00000000000..b154d78afab
--- /dev/null
+++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.enterprise.impl
+
+import androidx.compose.ui.graphics.Color
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.compound.colors.SemanticColorsLightDark
+import io.element.android.features.enterprise.api.BugReportUrl
+import io.element.android.features.enterprise.api.EnterpriseService
+import io.element.android.libraries.matrix.api.core.SessionId
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+@ContributesBinding(AppScope::class)
+class DefaultEnterpriseService : EnterpriseService {
+ override val isEnterpriseBuild = false
+
+ override suspend fun isEnterpriseUser(sessionId: SessionId) = false
+
+ override fun defaultHomeserverList(): List = emptyList()
+ override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
+
+ override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
+
+ override fun brandColorsFlow(sessionId: SessionId?): Flow {
+ return flowOf(null)
+ }
+
+ override fun semanticColorsFlow(sessionId: SessionId?): Flow {
+ return flowOf(SemanticColorsLightDark.default)
+ }
+
+ override fun firebasePushGateway(): String? = null
+ override fun unifiedPushDefaultPushGateway(): String? = null
+
+ override fun bugReportUrlFlow(sessionId: SessionId?): Flow {
+ return flowOf(BugReportUrl.UseDefault)
+ }
+}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt
new file mode 100644
index 00000000000..3441063a8a3
--- /dev/null
+++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.enterprise.impl
+
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.enterprise.api.SessionEnterpriseService
+import io.element.android.libraries.di.SessionScope
+
+@ContributesBinding(SessionScope::class)
+class DefaultSessionEnterpriseService : SessionEnterpriseService {
+ override suspend fun init() = Unit
+ override suspend fun isElementCallAvailable(): Boolean = true
+}
diff --git a/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt
new file mode 100644
index 00000000000..ff95fd8721d
--- /dev/null
+++ b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.enterprise.impl
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.compound.colors.SemanticColorsLightDark
+import io.element.android.features.enterprise.api.BugReportUrl
+import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultEnterpriseServiceTest {
+ @Test
+ fun `isEnterpriseBuild is false`() {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
+ }
+
+ @Test
+ fun `defaultHomeserverList should return empty list`() {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
+ }
+
+ @Test
+ fun `isAllowedToConnectToHomeserver is true for all homeserver urls`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.isAllowedToConnectToHomeserver(A_HOMESERVER_URL)).isTrue()
+ }
+
+ @Test
+ fun `isEnterpriseUser always return false`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse()
+ }
+
+ @Test
+ fun `semanticColorsFlow always emits the same value`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ defaultEnterpriseService.semanticColorsFlow(null).test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `brandColorsFlow always emits null`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ defaultEnterpriseService.brandColorsFlow(null).test {
+ val initialState = awaitItem()
+ assertThat(initialState).isNull()
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `semanticColorsFlow always emits the same value for a session`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ defaultEnterpriseService.semanticColorsFlow(A_SESSION_ID).test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `overrideBrandColor has no effect`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "aColor")
+ }
+
+ @Test
+ fun `firebasePushGateway returns null`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.firebasePushGateway()).isNull()
+ }
+
+ @Test
+ fun `unifiedPushDefaultPushGateway returns null`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ assertThat(defaultEnterpriseService.unifiedPushDefaultPushGateway()).isNull()
+ }
+
+ @Test
+ fun `bugReportUrlFlow only emits UseDefault`() = runTest {
+ val defaultEnterpriseService = DefaultEnterpriseService()
+ defaultEnterpriseService.bugReportUrlFlow(A_SESSION_ID).test {
+ assertThat(awaitItem()).isEqualTo(BugReportUrl.UseDefault)
+ awaitComplete()
+ }
+ }
+}
diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt
similarity index 91%
rename from features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt
rename to features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt
index b1e70ef045c..391878a0a4e 100644
--- a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt
+++ b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/enterprise/impl/build.gradle.kts b/features/enterprise/impl/build.gradle.kts
deleted file mode 100644
index 642b2fef40f..00000000000
--- a/features/enterprise/impl/build.gradle.kts
+++ /dev/null
@@ -1,30 +0,0 @@
-import extension.setupAnvil
-
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-plugins {
- id("io.element.android-library")
-}
-
-android {
- namespace = "io.element.android.features.enterprise.impl"
-}
-
-setupAnvil()
-
-dependencies {
- implementation(libs.compound)
- implementation(projects.anvilannotations)
- api(projects.features.enterprise.api)
- implementation(projects.libraries.architecture)
- implementation(projects.libraries.matrix.api)
-
- testImplementation(libs.coroutines.test)
- testImplementation(libs.test.junit)
- testImplementation(libs.test.truth)
- testImplementation(projects.libraries.matrix.test)
-}
diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
deleted file mode 100644
index 7f4f4b387b3..00000000000
--- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.enterprise.impl
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.compound.tokens.generated.SemanticColors
-import io.element.android.compound.tokens.generated.compoundColorsDark
-import io.element.android.compound.tokens.generated.compoundColorsLight
-import io.element.android.features.enterprise.api.EnterpriseService
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.matrix.api.core.SessionId
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
- override val isEnterpriseBuild = false
-
- override suspend fun isEnterpriseUser(sessionId: SessionId) = false
-
- override fun defaultHomeserverList(): List = emptyList()
- override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
-
- override fun semanticColorsLight(): SemanticColors = compoundColorsLight
-
- override fun semanticColorsDark(): SemanticColors = compoundColorsDark
-
- override fun firebasePushGateway(): String? = null
- override fun unifiedPushDefaultPushGateway(): String? = null
-}
diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt
deleted file mode 100644
index 2728bdad6ae..00000000000
--- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.enterprise.impl
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.enterprise.api.SessionEnterpriseService
-import io.element.android.libraries.di.SessionScope
-import javax.inject.Inject
-
-@ContributesBinding(SessionScope::class)
-class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService {
- override suspend fun isElementCallAvailable(): Boolean = true
-}
diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt
deleted file mode 100644
index 9f3ebbb8454..00000000000
--- a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.enterprise.impl
-
-import com.google.common.truth.Truth.assertThat
-import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
-import io.element.android.libraries.matrix.test.A_SESSION_ID
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class DefaultEnterpriseServiceTest {
- @Test
- fun `isEnterpriseBuild is false`() {
- val defaultEnterpriseService = DefaultEnterpriseService()
- assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
- }
-
- @Test
- fun `defaultHomeserverList should return empty list`() {
- val defaultEnterpriseService = DefaultEnterpriseService()
- assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
- }
-
- @Test
- fun `isAllowedToConnectToHomeserver is true for all homeserver urls`() = runTest {
- val defaultEnterpriseService = DefaultEnterpriseService()
- assertThat(defaultEnterpriseService.isAllowedToConnectToHomeserver(A_HOMESERVER_URL)).isTrue()
- }
-
- @Test
- fun `isEnterpriseUser always return false`() = runTest {
- val defaultEnterpriseService = DefaultEnterpriseService()
- assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse()
- }
-}
diff --git a/features/enterprise/test/build.gradle.kts b/features/enterprise/test/build.gradle.kts
index 91b76f4fa78..542e73717a8 100644
--- a/features/enterprise/test/build.gradle.kts
+++ b/features/enterprise/test/build.gradle.kts
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
@@ -14,7 +15,7 @@ android {
dependencies {
api(projects.features.enterprise.api)
- implementation(libs.compound)
+ implementation(projects.libraries.compound)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}
diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
index 14195e12432..b2c386267ad 100644
--- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
+++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
@@ -1,28 +1,38 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test
-import io.element.android.compound.tokens.generated.SemanticColors
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.colors.SemanticColorsLightDark
+import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
- private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
- private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
+ initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
+ initialBrandColor: Color? = null,
+ private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService {
+ private val brandColorState = MutableStateFlow(initialBrandColor)
+ private val semanticColorsState = MutableStateFlow(initialSemanticColors)
+
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
isEnterpriseUserResult(sessionId)
}
@@ -35,12 +45,16 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
- override fun semanticColorsLight(): SemanticColors {
- return semanticColorsLightResult()
+ override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
+ overrideBrandColorResult(sessionId, brandColor)
+ }
+
+ override fun brandColorsFlow(sessionId: SessionId?): Flow {
+ return brandColorState.asStateFlow()
}
- override fun semanticColorsDark(): SemanticColors {
- return semanticColorsDarkResult()
+ override fun semanticColorsFlow(sessionId: SessionId?): Flow {
+ return semanticColorsState.asStateFlow()
}
override fun firebasePushGateway(): String? {
@@ -51,7 +65,8 @@ class FakeEnterpriseService(
return unifiedPushDefaultPushGatewayResult()
}
- companion object {
- const val A_FAKE_HOMESERVER = "a_fake_homeserver"
+ val bugReportUrlMutableFlow = MutableStateFlow(BugReportUrl.UseDefault)
+ override fun bugReportUrlFlow(sessionId: SessionId?): Flow {
+ return bugReportUrlMutableFlow.asStateFlow()
}
}
diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt
index a1e811ed107..3914c601557 100644
--- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt
+++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,6 +15,9 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeSessionEnterpriseService(
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
) : SessionEnterpriseService {
+ override suspend fun init() {
+ }
+
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult()
}
diff --git a/features/forward/api/build.gradle.kts b/features/forward/api/build.gradle.kts
new file mode 100644
index 00000000000..1c5f47a0487
--- /dev/null
+++ b/features/forward/api/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.forward.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt
new file mode 100644
index 00000000000..95b8c43808c
--- /dev/null
+++ b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.api
+
+import com.bumble.appyx.core.modality.BuildContext
+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.NodeInputs
+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.timeline.TimelineProvider
+
+interface ForwardEntryPoint : FeatureEntryPoint {
+ interface Callback : Plugin {
+ fun onDone(roomIds: List)
+ }
+
+ data class Params(
+ val eventId: EventId,
+ val timelineProvider: TimelineProvider,
+ ) : NodeInputs
+
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: Params,
+ callback: Callback,
+ ): Node
+}
diff --git a/features/forward/impl/build.gradle.kts b/features/forward/impl/build.gradle.kts
new file mode 100644
index 00000000000..5d692c861e9
--- /dev/null
+++ b/features/forward/impl/build.gradle.kts
@@ -0,0 +1,41 @@
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
+
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.forward.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupDependencyInjection()
+
+dependencies {
+ api(projects.features.forward.api)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.roomselect.api)
+ implementation(projects.libraries.uiStrings)
+
+ testCommonDependencies(libs, true)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.roomselect.test)
+ testImplementation(projects.libraries.testtags)
+}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt
new file mode 100644
index 00000000000..97256c65767
--- /dev/null
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.forward.api.ForwardEntryPoint
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.SessionScope
+
+@ContributesBinding(SessionScope::class)
+class DefaultForwardEntryPoint : ForwardEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: ForwardEntryPoint.Params,
+ callback: ForwardEntryPoint.Callback,
+ ): Node {
+ return parentNode.createNode(
+ buildContext = buildContext,
+ plugins = listOf(
+ ForwardMessagesNode.Inputs(
+ eventId = params.eventId,
+ timelineProvider = params.timelineProvider,
+ ),
+ callback,
+ )
+ )
+ }
+}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt
new file mode 100644
index 00000000000..0161a5d6571
--- /dev/null
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+sealed interface ForwardMessagesEvents {
+ data object ClearError : ForwardMessagesEvents
+}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt
new file mode 100644
index 00000000000..eb085d19c55
--- /dev/null
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.composable.Children
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.node.ParentNode
+import com.bumble.appyx.core.plugin.Plugin
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.features.forward.api.ForwardEntryPoint
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.SessionScope
+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.timeline.TimelineProvider
+import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
+import io.element.android.libraries.roomselect.api.RoomSelectMode
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(SessionScope::class)
+@AssistedInject
+class ForwardMessagesNode(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: ForwardMessagesPresenter.Factory,
+ private val roomSelectEntryPoint: RoomSelectEntryPoint,
+) : ParentNode(
+ navModel = PermanentNavModel(
+ navTargets = setOf(NavTarget),
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ buildContext = buildContext,
+ plugins = plugins,
+) {
+ @Parcelize
+ object NavTarget : Parcelable
+
+ data class Inputs(
+ val eventId: EventId,
+ val timelineProvider: TimelineProvider,
+ ) : NodeInputs
+
+ private val inputs = inputs()
+ private val callback: ForwardEntryPoint.Callback = callback()
+ private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider)
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ val callback = object : RoomSelectEntryPoint.Callback {
+ override fun onRoomSelected(roomIds: List) {
+ presenter.onRoomSelected(roomIds)
+ }
+
+ override fun onCancel() {
+ callback.onDone(emptyList())
+ }
+ }
+
+ return roomSelectEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward),
+ callback = callback,
+ )
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ Box(modifier = modifier) {
+ // Will render to room select screen
+ Children(
+ navModel = navModel,
+ )
+
+ val state = presenter.present()
+ ForwardMessagesView(
+ state = state,
+ onForwardSuccess = callback::onDone,
+ )
+ }
+ }
+}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt
new file mode 100644
index 00000000000..1145f294a31
--- /dev/null
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.runCatchingUpdatingState
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
+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.timeline.TimelineProvider
+import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+@AssistedInject
+class ForwardMessagesPresenter(
+ @Assisted eventId: String,
+ @Assisted private val timelineProvider: TimelineProvider,
+ @SessionCoroutineScope
+ private val sessionCoroutineScope: CoroutineScope,
+) : Presenter {
+ private val eventId: EventId = EventId(eventId)
+
+ @AssistedFactory
+ fun interface Factory {
+ fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter
+ }
+
+ private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized)
+
+ fun onRoomSelected(roomIds: List) {
+ sessionCoroutineScope.forwardEvent(eventId, roomIds)
+ }
+
+ @Composable
+ override fun present(): ForwardMessagesState {
+ fun handleEvent(event: ForwardMessagesEvents) {
+ when (event) {
+ ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
+ }
+ }
+
+ return ForwardMessagesState(
+ forwardAction = forwardingActionState.value,
+ eventSink = ::handleEvent,
+ )
+ }
+
+ private fun CoroutineScope.forwardEvent(
+ eventId: EventId,
+ roomIds: List,
+ ) = launch {
+ suspend {
+ timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds)
+ .onFailure {
+ Timber.e(it, "Error while forwarding event")
+ }
+ .getOrThrow()
+ roomIds
+ }.runCatchingUpdatingState(forwardingActionState)
+ }
+}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt
new file mode 100644
index 00000000000..b1e46adb03f
--- /dev/null
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.core.RoomId
+
+data class ForwardMessagesState(
+ val forwardAction: AsyncAction>,
+ val eventSink: (ForwardMessagesEvents) -> Unit
+)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt
similarity index 89%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt
index b1728f46572..bbb5b380ae4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.forward
+package io.element.android.features.forward.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
similarity index 76%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
index 9ea76d754f6..8065054b2dd 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
@@ -1,18 +1,21 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.forward
+package io.element.android.features.forward.impl
import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.async.AsyncActionView
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.ui.strings.CommonStrings
@Composable
fun ForwardMessagesView(
@@ -24,6 +27,9 @@ fun ForwardMessagesView(
onSuccess = {
onForwardSuccess(it)
},
+ errorMessage = {
+ stringResource(id = CommonStrings.error_unknown)
+ },
onErrorDismiss = {
state.eventSink(ForwardMessagesEvents.ClearError)
},
diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt
new file mode 100644
index 00000000000..9ee932a246b
--- /dev/null
+++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.forward.api.ForwardEntryPoint
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
+import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultForwardEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder`() = runTest {
+ val entryPoint = DefaultForwardEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ ForwardMessagesNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenterFactory = { _, _ -> createForwardMessagesPresenter() },
+ roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
+ )
+ }
+ val callback = object : ForwardEntryPoint.Callback {
+ override fun onDone(roomIds: List) = lambdaError()
+ }
+ val params = ForwardEntryPoint.Params(
+ eventId = AN_EVENT_ID,
+ timelineProvider = FakeTimelineProvider(),
+ )
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ params = params,
+ callback = callback,
+ )
+ assertThat(result).isInstanceOf(ForwardMessagesNode::class.java)
+ assertThat(result.plugins).contains(
+ ForwardMessagesNode.Inputs(
+ eventId = params.eventId,
+ timelineProvider = params.timelineProvider,
+ )
+ )
+ assertThat(result.plugins).contains(callback)
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
similarity index 84%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
rename to features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
index 757e682592e..5e2f74ccade 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
+++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.forward
+package io.element.android.features.forward.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
@@ -32,7 +33,7 @@ class ForwardMessagesPresenterTest {
@Test
fun `present - initial state`() = runTest {
- val presenter = aForwardMessagesPresenter()
+ val presenter = createForwardMessagesPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -50,7 +51,7 @@ class ForwardMessagesPresenterTest {
this.forwardEventLambda = forwardEventLambda
}
val room = FakeJoinedRoom(liveTimeline = timeline)
- val presenter = aForwardMessagesPresenter(fakeRoom = room)
+ val presenter = createForwardMessagesPresenter(fakeRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -74,7 +75,7 @@ class ForwardMessagesPresenterTest {
this.forwardEventLambda = forwardEventLambda
}
val room = FakeJoinedRoom(liveTimeline = timeline)
- val presenter = aForwardMessagesPresenter(fakeRoom = room)
+ val presenter = createForwardMessagesPresenter(fakeRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -90,13 +91,13 @@ class ForwardMessagesPresenterTest {
forwardEventLambda.assertions().isCalledOnce()
}
}
-
- private fun TestScope.aForwardMessagesPresenter(
- eventId: EventId = AN_EVENT_ID,
- fakeRoom: FakeJoinedRoom = FakeJoinedRoom(),
- ) = ForwardMessagesPresenter(
- eventId = eventId.value,
- timelineProvider = LiveTimelineProvider(fakeRoom),
- sessionCoroutineScope = this,
- )
}
+
+fun TestScope.createForwardMessagesPresenter(
+ eventId: EventId = AN_EVENT_ID,
+ fakeRoom: FakeJoinedRoom = FakeJoinedRoom(),
+) = ForwardMessagesPresenter(
+ eventId = eventId.value,
+ timelineProvider = LiveTimelineProvider(fakeRoom),
+ sessionCoroutineScope = this,
+)
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
similarity index 94%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt
rename to features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
index 0912fcc75c1..f1e9bd8fc66 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt
+++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
@@ -1,11 +1,12 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.messages.impl.forward
+package io.element.android.features.forward.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
diff --git a/features/forward/test/build.gradle.kts b/features/forward/test/build.gradle.kts
new file mode 100644
index 00000000000..bf9db05e341
--- /dev/null
+++ b/features/forward/test/build.gradle.kts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.forward.test"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.features.forward.api)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/forward/test/src/main/kotlin/io/element/android/features/forward/test/FakeForwardEntryPoint.kt b/features/forward/test/src/main/kotlin/io/element/android/features/forward/test/FakeForwardEntryPoint.kt
new file mode 100644
index 00000000000..9306ebcb295
--- /dev/null
+++ b/features/forward/test/src/main/kotlin/io/element/android/features/forward/test/FakeForwardEntryPoint.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.forward.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.forward.api.ForwardEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeForwardEntryPoint : ForwardEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: ForwardEntryPoint.Params,
+ callback: ForwardEntryPoint.Callback,
+ ): Node = lambdaError()
+}
diff --git a/features/ftue/api/build.gradle.kts b/features/ftue/api/build.gradle.kts
index 933dbbd62ae..4f1d8b14bc9 100644
--- a/features/ftue/api/build.gradle.kts
+++ b/features/ftue/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt
index f719064fde0..fe5ffc3b44a 100644
--- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt
+++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt
@@ -1,20 +1,13 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.api
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
-interface FtueEntryPoint : FeatureEntryPoint {
- fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
-
- interface NodeBuilder {
- fun build(): Node
- }
-}
+interface FtueEntryPoint : SimpleFeatureEntryPoint
diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt
index 7ea26c548f1..1bc585f015e 100644
--- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt
+++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,9 +16,6 @@ import kotlinx.coroutines.flow.StateFlow
interface FtueService {
/** The current state of the FTUE. */
val state: StateFlow
-
- /** Reset the FTUE state. */
- suspend fun reset()
}
/** The state of the FTUE. */
diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts
index e95a4697ae9..49b3baa71b4 100644
--- a/features/ftue/impl/build.gradle.kts
+++ b/features/ftue/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,7 +24,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.ftue.api)
@@ -33,6 +35,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.preferences.api)
+ implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
implementation(projects.features.analytics.api)
@@ -46,14 +49,7 @@ dependencies {
implementation(projects.services.toolbox.api)
implementation(projects.appconfig)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+ testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.analytics.noop)
@@ -61,5 +57,4 @@ dependencies {
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.services.toolbox.test)
- testImplementation(projects.tests.testutils)
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt
index ee0a3d87ef7..812910e1828 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,22 +10,14 @@ package io.element.android.features.ftue.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint {
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): FtueEntryPoint.NodeBuilder {
- val plugins = ArrayList()
-
- return object : FtueEntryPoint.NodeBuilder {
- override fun build(): Node {
- return parentNode.createNode(buildContext, plugins)
- }
- }
+class DefaultFtueEntryPoint : FtueEntryPoint {
+ override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
+ return parentNode.createNode(buildContext)
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
index 71a27dcaae1..8d66258e129 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
@@ -1,56 +1,50 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl
import android.os.Parcelable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
-import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.replace
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
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.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep
+import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
-import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
-import io.element.android.services.analytics.api.AnalyticsService
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
+import io.element.android.libraries.ui.common.nodes.emptyNode
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class FtueFlowNode @AssistedInject constructor(
+@AssistedInject
+class FtueFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val ftueState: DefaultFtueService,
+ private val defaultFtueService: DefaultFtueService,
private val analyticsEntryPoint: AnalyticsEntryPoint,
- private val analyticsService: AnalyticsService,
private val lockScreenEntryPoint: LockScreenEntryPoint,
) : BaseFlowNode(
backstack = BackStack(
@@ -79,31 +73,23 @@ class FtueFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
-
- lifecycle.subscribe(onCreate = {
- moveToNextStepIfNeeded()
- })
-
- analyticsService.didAskUserConsentFlow
- .distinctUntilChanged()
- .onEach { moveToNextStepIfNeeded() }
- .launchIn(lifecycleScope)
-
- ftueState.isVerificationStatusKnown
- .filter { it }
- .onEach { moveToNextStepIfNeeded() }
+ defaultFtueService.ftueStepStateFlow
+ .filterIsInstance(InternalFtueState.Incomplete::class)
+ .onEach {
+ showStep(it.nextStep)
+ }
.launchIn(lifecycleScope)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Placeholder -> {
- createNode(buildContext)
+ emptyNode(buildContext)
}
is NavTarget.SessionVerification -> {
val callback = object : FtueSessionVerificationFlowNode.Callback {
override fun onDone() {
- moveToNextStepIfNeeded()
+ defaultFtueService.onUserCompletedSessionVerification()
}
}
createNode(buildContext, listOf(callback))
@@ -111,7 +97,7 @@ class FtueFlowNode @AssistedInject constructor(
NavTarget.NotificationsOptIn -> {
val callback = object : NotificationsOptInNode.Callback {
override fun onNotificationsOptInFinished() {
- moveToNextStepIfNeeded()
+ defaultFtueService.updateFtueStep()
}
}
createNode(buildContext, listOf(callback))
@@ -122,18 +108,21 @@ class FtueFlowNode @AssistedInject constructor(
NavTarget.LockScreenSetup -> {
val callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() {
- moveToNextStepIfNeeded()
+ defaultFtueService.updateFtueStep()
}
}
- lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
- .callback(callback)
- .build()
+ lockScreenEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ navTarget = LockScreenEntryPoint.Target.Setup,
+ callback = callback,
+ )
}
}
}
- private fun moveToNextStepIfNeeded() = lifecycleScope.launch {
- when (ftueState.getNextStep()) {
+ private fun showStep(ftueStep: FtueStep) {
+ when (ftueStep) {
FtueStep.WaitingForInitialState -> {
backstack.newRoot(NavTarget.Placeholder)
}
@@ -149,7 +138,6 @@ class FtueFlowNode @AssistedInject constructor(
FtueStep.LockscreenSetup -> {
backstack.newRoot(NavTarget.LockScreenSetup)
}
- null -> Unit
}
}
@@ -157,17 +145,4 @@ class FtueFlowNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
BackstackView()
}
-
- @ContributesNode(AppScope::class)
- class PlaceholderNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- ) : Node(buildContext, plugins = plugins) {
- @Composable
- override fun View(modifier: Modifier) {
- Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- }
- }
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt
index 4387b3dc00f..52f7a43bde0 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt
@@ -1,22 +1,23 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModePresenter
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
-@Module
+@BindingContainer
interface FtueModule {
@Binds
fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt
index dd133f8fd83..e0bd5acf430 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt
index 6df334b6df0..52ad70b242e 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,15 +13,16 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
-class NotificationsOptInNode @AssistedInject constructor(
+@AssistedInject
+class NotificationsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
presenterFactory: NotificationsOptInPresenter.Factory,
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
index 5db0955c5ac..344d83c38f4 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,9 +13,9 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
@@ -25,7 +26,8 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-class NotificationsOptInPresenter @AssistedInject constructor(
+@AssistedInject
+class NotificationsOptInPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,
@AppCoroutineScope
@@ -50,7 +52,7 @@ class NotificationsOptInPresenter @AssistedInject constructor(
override fun present(): NotificationsOptInState {
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
- fun handleEvents(event: NotificationsOptInEvents) {
+ fun handleEvent(event: NotificationsOptInEvents) {
when (event) {
NotificationsOptInEvents.ContinueClicked -> {
if (notificationsPermissionsState.permissionGranted) {
@@ -77,7 +79,7 @@ class NotificationsOptInPresenter @AssistedInject constructor(
return NotificationsOptInState(
notificationsPermissionState = notificationsPermissionsState,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
index 92074d75ee8..d12cec07786 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt
index 265c4e8a994..1b2e5d2d581 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
index 444c311a637..6955d8e501b 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -31,10 +32,10 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
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.IconTitleSubtitleMolecule
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.PageTitle
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.AvatarSize
@@ -59,7 +60,7 @@ fun NotificationsOptInView(
.statusBarsPadding()
.fillMaxSize(),
background = { OnboardingBackground() },
- header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
+ header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 28.dp)) },
footer = { NotificationsOptInFooter(state) },
) {
NotificationsOptInContent()
@@ -70,10 +71,10 @@ fun NotificationsOptInView(
private fun NotificationsOptInHeader(
modifier: Modifier = Modifier,
) {
- PageTitle(
+ IconTitleSubtitleMolecule(
modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title),
- subtitle = stringResource(R.string.screen_notification_optin_subtitle),
+ subTitle = stringResource(R.string.screen_notification_optin_subtitle),
iconStyle = BigIcon.Style.Default(CompoundIcons.NotificationsSolid()),
)
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
index a37fdd21922..085240d83e9 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,20 +16,20 @@ import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.utils.OpenUrlInTabView
import io.element.android.libraries.di.SessionScope
@@ -37,7 +38,8 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class FtueSessionVerificationFlowNode @AssistedInject constructor(
+@AssistedInject
+class FtueSessionVerificationFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
@@ -68,6 +70,8 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
fun onDone()
}
+ private val callback: Callback = callback()
+
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
override fun onDone() {
lifecycleScope.launch {
@@ -81,62 +85,67 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
return when (navTarget) {
is NavTarget.Root -> {
val callback = object : ChooseSelfVerificationModeNode.Callback {
- override fun onUseAnotherDevice() {
+ override fun navigateToUseAnotherDevice() {
backstack.push(NavTarget.UseAnotherDevice)
}
- override fun onUseRecoveryKey() {
+ override fun navigateToUseRecoveryKey() {
backstack.push(NavTarget.EnterRecoveryKey)
}
- override fun onResetKey() {
+ override fun navigateToResetKey() {
backstack.push(NavTarget.ResetIdentity)
}
- override fun onLearnMoreAboutEncryption() {
+ override fun navigateToLearnMoreAboutEncryption() {
learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL
}
}
-
createNode(buildContext, plugins = listOf(callback))
}
is NavTarget.UseAnotherDevice -> {
- outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
- .params(OutgoingVerificationEntryPoint.Params(
+ outgoingVerificationEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = true,
verificationRequest = VerificationRequest.Outgoing.CurrentSession,
- ))
- .callback(object : OutgoingVerificationEntryPoint.Callback {
+ ),
+ callback = object : OutgoingVerificationEntryPoint.Callback {
override fun onDone() {
- plugins().forEach { it.onDone() }
+ callback.onDone()
}
override fun onBack() {
backstack.pop()
}
- override fun onLearnMoreAboutEncryption() {
+ override fun navigateToLearnMoreAboutEncryption() {
// Note that this callback is never called. The "Learn more" link is not displayed
// for the self session interactive verification.
}
- })
- .build()
+ }
+ )
}
is NavTarget.EnterRecoveryKey -> {
- secureBackupEntryPoint.nodeBuilder(this, buildContext)
- .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
- .callback(secureBackupEntryPointCallback)
- .build()
+ secureBackupEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey),
+ callback = secureBackupEntryPointCallback
+ )
}
is NavTarget.ResetIdentity -> {
- secureBackupEntryPoint.nodeBuilder(this, buildContext)
- .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity))
- .callback(object : SecureBackupEntryPoint.Callback {
+ secureBackupEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity),
+ callback = object : SecureBackupEntryPoint.Callback {
override fun onDone() {
- plugins().forEach { it.onDone() }
+ callback.onDone()
}
- })
- .build()
+ },
+ )
}
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt
index 99e59506937..de7e1767f58 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt
index 687dc4aefeb..e78eef7edb8 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,29 +13,30 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class ChooseSelfVerificationModeNode @AssistedInject constructor(
+@AssistedInject
+class ChooseSelfVerificationModeNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: Presenter,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
- fun onUseAnotherDevice()
- fun onUseRecoveryKey()
- fun onResetKey()
- fun onLearnMoreAboutEncryption()
+ fun navigateToUseAnotherDevice()
+ fun navigateToUseRecoveryKey()
+ fun navigateToResetKey()
+ fun navigateToLearnMoreAboutEncryption()
}
- private val callback = plugins().first()
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
@@ -42,10 +44,10 @@ class ChooseSelfVerificationModeNode @AssistedInject constructor(
ChooseSelfVerificationModeView(
state = state,
- onUseAnotherDevice = callback::onUseAnotherDevice,
- onUseRecoveryKey = callback::onUseRecoveryKey,
- onResetKey = callback::onResetKey,
- onLearnMore = callback::onLearnMoreAboutEncryption,
+ onUseAnotherDevice = callback::navigateToUseAnotherDevice,
+ onUseRecoveryKey = callback::navigateToUseRecoveryKey,
+ onResetKey = callback::navigateToResetKey,
+ onLearnMore = callback::navigateToLearnMoreAboutEncryption,
modifier = modifier,
)
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
index 0aec3b4e1d1..ace890be9c7 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,36 +13,63 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
-import javax.inject.Inject
-class ChooseSelfVerificationModePresenter @Inject constructor(
+@Inject
+class ChooseSelfVerificationModePresenter(
private val encryptionService: EncryptionService,
private val directLogoutPresenter: Presenter,
) : Presenter {
@Composable
override fun present(): ChooseSelfVerificationModeState {
- val isLastDevice by encryptionService.isLastDevice.collectAsState()
- val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
- val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
+ val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
+ val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
+ .mapState { recoveryState ->
+ when (recoveryState) {
+ RecoveryState.WAITING_FOR_SYNC,
+ RecoveryState.UNKNOWN -> AsyncData.Loading()
+ RecoveryState.INCOMPLETE -> AsyncData.Success(true)
+ RecoveryState.ENABLED,
+ RecoveryState.DISABLED -> AsyncData.Success(false)
+ }
+ }
+ .collectAsState()
+ val buttonsState by remember {
+ derivedStateOf {
+ val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
+ val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
+ if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
+ AsyncData.Loading()
+ } else {
+ AsyncData.Success(
+ ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = canUseAnotherDevice,
+ canEnterRecoveryKey = canEnterRecoveryKey,
+ )
+ )
+ }
+ }
+ }
val directLogoutState = directLogoutPresenter.present()
- fun eventHandler(event: ChooseSelfVerificationModeEvent) {
+ fun handleEvent(event: ChooseSelfVerificationModeEvent) {
when (event) {
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
}
}
return ChooseSelfVerificationModeState(
- isLastDevice = isLastDevice,
- canEnterRecoveryKey = canEnterRecoveryKey,
+ buttonsState = buttonsState,
directLogoutState = directLogoutState,
- eventSink = ::eventHandler,
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
index 21c37a4ae29..4e27043f40c 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt
@@ -1,17 +1,23 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.sessionverification.choosemode
import io.element.android.features.logout.api.direct.DirectLogoutState
+import io.element.android.libraries.architecture.AsyncData
data class ChooseSelfVerificationModeState(
- val isLastDevice: Boolean,
- val canEnterRecoveryKey: Boolean,
+ val buttonsState: AsyncData,
val directLogoutState: DirectLogoutState,
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
-)
+) {
+ data class ButtonsState(
+ val canUseAnotherDevice: Boolean,
+ val canEnterRecoveryKey: Boolean,
+ )
+}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
index 574aa367c61..676c77211f2 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,23 +10,49 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.logout.api.direct.aDirectLogoutState
+import io.element.android.libraries.architecture.AsyncData
class ChooseSelfVerificationModeStateProvider :
PreviewParameterProvider {
override val values = sequenceOf(
- aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = true),
- aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = false),
- aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = true),
- aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = false),
+ aChooseSelfVerificationModeState(
+ buttonsState = AsyncData.Success(
+ aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
+ ),
+ ),
+ aChooseSelfVerificationModeState(
+ buttonsState = AsyncData.Success(
+ aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
+ ),
+ ),
+ aChooseSelfVerificationModeState(
+ buttonsState = AsyncData.Success(
+ aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
+ ),
+ ),
+ aChooseSelfVerificationModeState(
+ buttonsState = AsyncData.Success(
+ aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
+ ),
+ ),
+ aChooseSelfVerificationModeState(
+ buttonsState = AsyncData.Loading(),
+ ),
)
}
fun aChooseSelfVerificationModeState(
- isLastDevice: Boolean = false,
- canEnterRecoveryKey: Boolean = true,
+ buttonsState: AsyncData = AsyncData.Success(aButtonsState()),
) = ChooseSelfVerificationModeState(
- isLastDevice = isLastDevice,
- canEnterRecoveryKey = canEnterRecoveryKey,
+ buttonsState = buttonsState,
directLogoutState = aDirectLogoutState(),
eventSink = {},
)
+
+fun aButtonsState(
+ canUseAnotherDevice: Boolean = true,
+ canEnterRecoveryKey: Boolean = true,
+) = ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = canUseAnotherDevice,
+ canEnterRecoveryKey = canEnterRecoveryKey,
+)
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
index 9839f3bf7c7..e4e922f9339 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -23,10 +24,11 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
-import io.element.android.libraries.designsystem.components.PageTitle
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
@@ -50,7 +52,6 @@ fun ChooseSelfVerificationModeView(
BackHandler {
activity?.finish()
}
-
HeaderFooterPage(
modifier = modifier,
topBar = {
@@ -65,24 +66,68 @@ fun ChooseSelfVerificationModeView(
)
},
header = {
- PageTitle(
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(bottom = 16.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
title = stringResource(id = R.string.screen_identity_confirmation_title),
- subtitle = stringResource(id = R.string.screen_identity_confirmation_subtitle)
+ subTitle = stringResource(id = R.string.screen_identity_confirmation_subtitle)
)
},
footer = {
- ButtonColumnMolecule(
- modifier = Modifier.padding(bottom = 16.dp)
- ) {
- if (state.isLastDevice.not()) {
+ ChooseSelfVerificationModeButtons(
+ state = state,
+ onUseAnotherDevice = onUseAnotherDevice,
+ onUseRecoveryKey = onUseRecoveryKey,
+ onResetKey = onResetKey,
+ )
+ }
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ modifier = Modifier
+ .clickable(onClick = onLearnMore)
+ .padding(vertical = 4.dp, horizontal = 16.dp),
+ text = stringResource(CommonStrings.action_learn_more),
+ style = ElementTheme.typography.fontBodyLgMedium
+ )
+ }
+ }
+}
+
+@Composable
+private fun ChooseSelfVerificationModeButtons(
+ state: ChooseSelfVerificationModeState,
+ onUseAnotherDevice: () -> Unit,
+ onUseRecoveryKey: () -> Unit,
+ onResetKey: () -> Unit,
+) {
+ ButtonColumnMolecule(
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ when (state.buttonsState) {
+ AsyncData.Uninitialized,
+ is AsyncData.Failure,
+ is AsyncData.Loading -> {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ enabled = false,
+ showProgress = true,
+ text = stringResource(CommonStrings.common_loading),
+ onClick = {},
+ )
+ }
+ is AsyncData.Success -> {
+ if (state.buttonsState.data.canUseAnotherDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_use_another_device),
onClick = onUseAnotherDevice,
)
}
- if (state.canEnterRecoveryKey) {
+ if (state.buttonsState.data.canEnterRecoveryKey) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
@@ -96,19 +141,6 @@ fun ChooseSelfVerificationModeView(
)
}
}
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- ) {
- Text(
- modifier = Modifier
- .clickable(onClick = onLearnMore)
- .padding(vertical = 4.dp, horizontal = 16.dp),
- text = stringResource(CommonStrings.action_learn_more),
- style = ElementTheme.typography.fontBodyLgMedium
- )
- }
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
index 744053b976a..78bb5f5c387 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,13 +10,13 @@ package io.element.android.features.ftue.impl.state
import android.Manifest
import android.os.Build
-import androidx.annotation.VisibleForTesting
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenService
+import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@@ -25,61 +26,69 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
-import javax.inject.Inject
+import kotlinx.coroutines.launch
@ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class)
-class DefaultFtueService @Inject constructor(
+class DefaultFtueService(
private val sdkVersionProvider: BuildVersionSdkIntProvider,
- @SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService,
private val sessionVerificationService: SessionVerificationService,
private val sessionPreferencesStore: SessionPreferencesStore,
) : FtueService {
- override val state = MutableStateFlow(FtueState.Unknown)
-
- /**
- * This flow emits true when the FTUE flow is ready to be displayed.
- * In this case, the FTUE flow is ready when the session verification status is known.
- */
- val isVerificationStatusKnown = sessionVerificationService.sessionVerifiedStatus
- .map { it != SessionVerifiedStatus.Unknown }
- .distinctUntilChanged()
-
- override suspend fun reset() {
- analyticsService.reset()
- if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
- permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
+ private val userNeedsToConfirmSessionVerificationSuccess = MutableStateFlow(false)
+
+ val ftueStepStateFlow = MutableStateFlow(InternalFtueState.Unknown)
+
+ override val state = ftueStepStateFlow
+ .mapState {
+ when (it) {
+ is InternalFtueState.Unknown -> FtueState.Unknown
+ is InternalFtueState.Incomplete -> FtueState.Incomplete
+ is InternalFtueState.Complete -> FtueState.Complete
+ }
}
- }
init {
- sessionVerificationService.sessionVerifiedStatus
- .onEach { updateState() }
+ combine(
+ sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus ->
+ if (sessionVerifiedStatus == SessionVerifiedStatus.NotVerified) {
+ // Ensure we wait for the user to confirm the session verified screen before going further
+ userNeedsToConfirmSessionVerificationSuccess.value = true
+ }
+ },
+ userNeedsToConfirmSessionVerificationSuccess,
+ analyticsService.didAskUserConsentFlow.distinctUntilChanged(),
+ ) {
+ updateFtueStep()
+ }
.launchIn(sessionCoroutineScope)
+ }
- analyticsService.didAskUserConsentFlow
- .distinctUntilChanged()
- .onEach { updateState() }
- .launchIn(sessionCoroutineScope)
+ fun updateFtueStep() = sessionCoroutineScope.launch {
+ val step = getNextStep(null)
+ ftueStepStateFlow.value = when (step) {
+ null -> InternalFtueState.Complete
+ else -> InternalFtueState.Incomplete(step)
+ }
}
- suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
- when (currentStep) {
+ private suspend fun getNextStep(completedStep: FtueStep? = null): FtueStep? =
+ when (completedStep) {
null -> if (!isSessionVerificationStateReady()) {
FtueStep.WaitingForInitialState
} else {
getNextStep(FtueStep.WaitingForInitialState)
}
- FtueStep.WaitingForInitialState -> if (isSessionNotVerified()) {
+ FtueStep.WaitingForInitialState -> if (isSessionNotVerified() || userNeedsToConfirmSessionVerificationSuccess.value) {
FtueStep.SessionVerification
} else {
getNextStep(FtueStep.SessionVerification)
@@ -107,9 +116,6 @@ class DefaultFtueService @Inject constructor(
}
private suspend fun isSessionNotVerified(): Boolean {
- // Wait until the session verification status is known
- isVerificationStatusKnown.filter { it }.first()
-
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification()
}
@@ -136,14 +142,8 @@ class DefaultFtueService @Inject constructor(
return lockScreenService.isSetupRequired().first()
}
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal suspend fun updateState() {
- val nextStep = getNextStep()
- state.value = when {
- // Final state, there aren't any more next steps
- nextStep == null -> FtueState.Complete
- else -> FtueState.Incomplete
- }
+ fun onUserCompletedSessionVerification() {
+ userNeedsToConfirmSessionVerificationSuccess.value = false
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.kt
new file mode 100644
index 00000000000..b352a43fe5f
--- /dev/null
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/InternalFtueState.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.ftue.impl.state
+
+sealed interface InternalFtueState {
+ data object Unknown : InternalFtueState
+
+ data class Incomplete(
+ val nextStep: FtueStep,
+ ) : InternalFtueState
+
+ data object Complete : InternalFtueState
+}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt
deleted file mode 100644
index a624db937fb..00000000000
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.ftue.impl.welcome
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.di.AppScope
-
-@ContributesNode(AppScope::class)
-class WelcomeNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- private val buildMeta: BuildMeta,
-) : Node(buildContext, plugins = plugins) {
- interface Callback : Plugin {
- fun onContinueClick()
- }
-
- private fun onContinueClick() {
- plugins.filterIsInstance().forEach { it.onContinueClick() }
- }
-
- @Composable
- override fun View(modifier: Modifier) {
- WelcomeView(
- applicationName = buildMeta.applicationName,
- onContinueClick = ::onContinueClick,
- modifier = modifier
- )
- }
-}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt
deleted file mode 100644
index b68f8e711b4..00000000000
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.ftue.impl.welcome
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.systemBarsPadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.ftue.impl.R
-import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
-import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
-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.pages.OnBoardingPage
-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.Text
-import io.element.android.libraries.testtags.TestTags
-import io.element.android.libraries.testtags.testTag
-import io.element.android.libraries.ui.strings.CommonStrings
-import kotlinx.collections.immutable.persistentListOf
-
-@Composable
-fun WelcomeView(
- applicationName: String,
- onContinueClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- BackHandler(onBack = onContinueClick)
- OnBoardingPage(
- modifier = modifier
- .systemBarsPadding()
- .fillMaxSize(),
- content = {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(rememberScrollState()),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Spacer(modifier = Modifier.height(78.dp))
- ElementLogoAtom(size = ElementLogoAtomSize.Medium)
- Spacer(modifier = Modifier.height(32.dp))
- Text(
- modifier = Modifier.testTag(TestTags.welcomeScreenTitle),
- text = stringResource(R.string.screen_welcome_title, applicationName),
- style = ElementTheme.typography.fontHeadingMdBold,
- color = ElementTheme.colors.textPrimary,
- textAlign = TextAlign.Center,
- )
- Spacer(modifier = Modifier.height(80.dp))
- InfoListOrganism(
- items = listItems(),
- textStyle = ElementTheme.typography.fontBodyMdMedium,
- iconTint = ElementTheme.colors.iconSecondary,
- backgroundColor = ElementTheme.colors.bgCanvasDefault.copy(alpha = 0.7f),
- )
- Spacer(modifier = Modifier.height(32.dp))
- }
- },
- footer = {
- Button(
- text = stringResource(CommonStrings.action_continue),
- modifier = Modifier.fillMaxWidth(),
- onClick = onContinueClick
- )
- Spacer(modifier = Modifier.height(32.dp))
- }
- )
-}
-
-@Composable
-private fun listItems() = persistentListOf(
- InfoListItem(
- message = stringResource(R.string.screen_welcome_bullet_2),
- iconVector = CompoundIcons.Lock(),
- ),
- InfoListItem(
- message = stringResource(R.string.screen_welcome_bullet_3),
- iconVector = CompoundIcons.ChatProblem(),
- ),
-)
-
-@PreviewsDayNight
-@Composable
-internal fun WelcomeViewPreview() {
- ElementPreview {
- WelcomeView(applicationName = "Element X", onContinueClick = {})
- }
-}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/SharedPreferencesWelcomeScreenStore.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/SharedPreferencesWelcomeScreenStore.kt
deleted file mode 100644
index 701d442acae..00000000000
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/SharedPreferencesWelcomeScreenStore.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.ftue.impl.welcome.state
-
-import android.content.SharedPreferences
-import androidx.core.content.edit
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-@SingleIn(AppScope::class)
-class SharedPreferencesWelcomeScreenStore @Inject constructor(
- private val sharedPreferences: SharedPreferences,
-) : WelcomeScreenStore {
- companion object {
- private const val IS_WELCOME_SCREEN_SHOWN = "is_welcome_screen_shown"
- }
-
- override fun isWelcomeScreenNeeded(): Boolean {
- return sharedPreferences.getBoolean(IS_WELCOME_SCREEN_SHOWN, false).not()
- }
-
- override fun setWelcomeScreenShown() {
- sharedPreferences.edit().putBoolean(IS_WELCOME_SCREEN_SHOWN, true).apply()
- }
-
- override fun reset() {
- sharedPreferences.edit {
- remove(IS_WELCOME_SCREEN_SHOWN)
- }
- }
-}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenStore.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenStore.kt
deleted file mode 100644
index a829feaf902..00000000000
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenStore.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.ftue.impl.welcome.state
-
-interface WelcomeScreenStore {
- fun isWelcomeScreenNeeded(): Boolean
- fun setWelcomeScreenShown()
- fun reset()
-}
diff --git a/features/ftue/impl/src/main/res/values-be/translations.xml b/features/ftue/impl/src/main/res/values-be/translations.xml
index 6c10902ca61..949410ce73a 100644
--- a/features/ftue/impl/src/main/res/values-be/translations.xml
+++ b/features/ftue/impl/src/main/res/values-be/translations.xml
@@ -13,10 +13,4 @@
"Вы можаце змяніць налады пазней.""Дазвольце апавяшчэнні і ніколі не прапускайце іх""Увядзіце ключ аднаўлення"
- "Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."
- "Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна."
- "Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад."
- "Пачнём!"
- "Вось што вам трэба ведаць:"
- "Вітаем у %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-bg/translations.xml b/features/ftue/impl/src/main/res/values-bg/translations.xml
index 7918ac7a0c5..0b7bd3de3ef 100644
--- a/features/ftue/impl/src/main/res/values-bg/translations.xml
+++ b/features/ftue/impl/src/main/res/values-bg/translations.xml
@@ -10,6 +10,4 @@
"Можете да промените настройките си по-късно.""Разрешете известията и никога не пропускайте съобщение""Въвеждане на ключ за възстановяване"
- "Хронологията на съобщенията за шифровани стаи все още не е налична."
- "Добре дошли в %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml
index 9d852feed61..6190f04faae 100644
--- a/features/ftue/impl/src/main/res/values-cs/translations.xml
+++ b/features/ftue/impl/src/main/res/values-cs/translations.xml
@@ -13,10 +13,4 @@
"Nastavení můžete později změnit.""Povolte oznámení a nezmeškejte žádnou zprávu""Zadejte klíč pro obnovení"
- "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."
- "Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."
- "Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."
- "Jdeme na to!"
- "Zde je to, co potřebujete vědět:"
- "Vítá vás %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-cy/translations.xml b/features/ftue/impl/src/main/res/values-cy/translations.xml
index bf87c2ca70c..c9ed3c04e10 100644
--- a/features/ftue/impl/src/main/res/values-cy/translations.xml
+++ b/features/ftue/impl/src/main/res/values-cy/translations.xml
@@ -13,10 +13,4 @@
"Gallwch newid eich gosodiadau yn nes ymlaen.""Caniatáu hysbysiadau a pheidio byth â cholli neges""Rhowch eich allwedd adfer"
- "Bydd galwadau, pleidleisiau, chwilio a mwy yn cael eu hychwanegu yn ddiweddarach eleni."
- "Nid yw hanes negeseuon ar gyfer ystafelloedd sydd wedi\'u hamgryptio ar gael eto."
- "Byddem wrth ein bodd yn clywed gennych, gadewch i ni wybod beth yw eich barn drwy\'r dudalen gosodiadau."
- "Ymlaen!"
- "Dyma beth sydd angen i chi ei wybod:"
- "Croeso i %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-da/translations.xml b/features/ftue/impl/src/main/res/values-da/translations.xml
index b1ae89a3b43..abbf00f49d8 100644
--- a/features/ftue/impl/src/main/res/values-da/translations.xml
+++ b/features/ftue/impl/src/main/res/values-da/translations.xml
@@ -13,10 +13,4 @@
"Du kan ændre dine indstillinger senere.""Tillad notifikationer, og gå aldrig glip af en besked""Indtast gendannelsesnøgle"
- "Opkald, afstemninger, søgninger og mere vil blive tilføjet senere på året."
- "Beskedhistorik for krypterede rum er ikke tilgængelig endnu."
- "Vi vil meget gerne høre fra dig. Fortæl os din mening via indstillingssiden."
- "Lad os komme i gang!"
- "Her er, hvad du har brug for at vide:"
- "Velkommen til %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml
index 10473d297e3..6d902ab8d4e 100644
--- a/features/ftue/impl/src/main/res/values-de/translations.xml
+++ b/features/ftue/impl/src/main/res/values-de/translations.xml
@@ -2,21 +2,15 @@
"Bestätigung unmöglich?""Erstelle einen neuen Wiederherstellungsschlüssel"
- "Verifiziere dieses Gerät, um sicheres Messaging einzurichten."
- "Bestätigen Sie Ihre Identität"
+ "Verifiziere dieses Gerät, um sichere Chats einzurichten."
+ "Bestätige deine Identität""Ein anderes Gerät verwenden""Wiederherstellungsschlüssel verwenden"
- "Sie können jetzt verschlüsselte Nachrichten lesen und versenden. Ihre Chatpartner vertrauen nun diesem Gerät auch."
+ "Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät.""Gerät verifiziert""Ein anderes Gerät verwenden""Bitte warten bis das andere Gerät bereit ist."
- "Sie können Ihre Einstellungen später ändern."
+ "Du kannst deine Einstellungen später ändern.""Erlaube Benachrichtigungen und verpasse keine Nachricht""Wiederherstellungsschlüssel eingeben"
- "Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."
- "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."
- "Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."
- "Los geht\'s!"
- "Folgendes sollten Sie wissen:"
- "Willkommen bei %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-el/translations.xml b/features/ftue/impl/src/main/res/values-el/translations.xml
index 585f5f0952d..f9029a7948b 100644
--- a/features/ftue/impl/src/main/res/values-el/translations.xml
+++ b/features/ftue/impl/src/main/res/values-el/translations.xml
@@ -13,10 +13,4 @@
"Μπορείς να αλλάξεις τις ρυθμίσεις σου αργότερα.""Επέτρεψε τις ειδοποιήσεις και μην χάσεις ούτε ένα μήνυμα""Εισαγωγή κλειδιού ανάκτησης"
- "Κλήσεις, δημοσκοπήσεις, αναζήτηση και άλλα, θα προστεθούν αργότερα φέτος."
- "Το ιστορικό μηνυμάτων για κρυπτογραφημένες αίθουσες δεν είναι ακόμη διαθέσιμο."
- "Θα θέλαμε να ακούσουμε τη γνώμη σου, πες μας τη γνώμη σου μέσω της σελίδας ρυθμίσεων."
- "Πάμε!"
- "Να τί πρέπει να ξέρεις:"
- "Καλώς ήρθες στο %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-es/translations.xml b/features/ftue/impl/src/main/res/values-es/translations.xml
index 36015aa6dd9..111821f0269 100644
--- a/features/ftue/impl/src/main/res/values-es/translations.xml
+++ b/features/ftue/impl/src/main/res/values-es/translations.xml
@@ -13,10 +13,4 @@
"Puedes cambiar la configuración más tarde.""Activa las notificaciones y nunca te pierdas un mensaje""Introduce la clave de recuperación"
- "Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."
- "El historial de mensajes de las salas cifradas aún no está disponible."
- "Nos encantaría saber de ti, haznos saber lo que piensas a través de la página de configuración."
- "¡Vamos!"
- "Esto es lo que necesitas saber:"
- "¡Bienvenido a %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-et/translations.xml b/features/ftue/impl/src/main/res/values-et/translations.xml
index 09762a4f631..c1898c3ce18 100644
--- a/features/ftue/impl/src/main/res/values-et/translations.xml
+++ b/features/ftue/impl/src/main/res/values-et/translations.xml
@@ -13,10 +13,4 @@
"Sa võid seadistusi hiljem alati muuta.""Luba teavitused ja kunagi ei jää sul sõnumid märkamata""Sisesta taastevõti"
- "Kõned, küsitlused, otsing ja palju muud lisanduvad hiljem selle aasta jooksul."
- "Krüptitud jututubade sõnumite ajalugu pole veel saadaval."
- "Me soovime teada mida sa arvad. Seadistuste lehel olevast valikust võid saata meile oma kommentaare."
- "Alustame!"
- "Sa peaksid teadma alljärgnevat:"
- "Tere tulemast rakendusse %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-eu/translations.xml b/features/ftue/impl/src/main/res/values-eu/translations.xml
index 2dcdff7330d..f512a62a33e 100644
--- a/features/ftue/impl/src/main/res/values-eu/translations.xml
+++ b/features/ftue/impl/src/main/res/values-eu/translations.xml
@@ -13,9 +13,4 @@
"Geroago alda ditzakezu ezarpenak.""Baimendu jakinarazpenak eta ez galdu inoiz mezurik""Sartu berreskuratze-gakoa"
- "Deiak, inkestak, bilaketa eta gehiago gehituko dira aurten."
- "Enkriptatutako geletarako mezuen historia ez dago oraindik erabilgarri."
- "Goazen!"
- "Hona hemen jakin beharrekoa:"
- "Ongi etorri %1$s(e)ra!"
diff --git a/features/ftue/impl/src/main/res/values-fa/translations.xml b/features/ftue/impl/src/main/res/values-fa/translations.xml
index 72151a11760..8c2b5ae2bcf 100644
--- a/features/ftue/impl/src/main/res/values-fa/translations.xml
+++ b/features/ftue/impl/src/main/res/values-fa/translations.xml
@@ -13,10 +13,4 @@
"میتوانید بعداً تنظیماتتان را تغییر دهید.""اجازه به آگاهیها و از دست ندادن پیامها""ورود کلید بازیابی"
- "تماس ها، نظرسنجی، جستجو و موارد دیگر در اواخر امسال اضافه خواهند شد."
- "سابقه پیام برای اتاق های رمزگذاری شده هنوز دردسترس نیست."
- "ما دوست داریم از شما بشنویم، نظر خود را از طریق صفحه تنظیمات با ما در میان بگذارید."
- "بزن بریم!"
- "چیزهایی که باید بدانید:"
- "به %1$s خوش آمدید!"
diff --git a/features/ftue/impl/src/main/res/values-fi/translations.xml b/features/ftue/impl/src/main/res/values-fi/translations.xml
index 3f3110bcdf9..cc3ee454f7e 100644
--- a/features/ftue/impl/src/main/res/values-fi/translations.xml
+++ b/features/ftue/impl/src/main/res/values-fi/translations.xml
@@ -13,10 +13,4 @@
"Voit muuttaa asetuksia myöhemmin.""Salli ilmoitukset ja älä koskaan missaa viestejä""Syötä palautusavain"
- "Puhelut, kyselyt, haku ja paljon muuta lisätään myöhemmin tänä vuonna."
- "Salattujen huoneiden viestihistoria ei ole vielä käytettävissä."
- "Haluaisimme kuulla mielipiteesi, kerro mitä mieltä olet asetuksien kautta."
- "Mennään!"
- "Tässä on mitä sinun tarvitsee tietää:"
- "Tervetuloa %1$s -sovellukseen!"
diff --git a/features/ftue/impl/src/main/res/values-fr/translations.xml b/features/ftue/impl/src/main/res/values-fr/translations.xml
index 04396014474..43bc6e0b448 100644
--- a/features/ftue/impl/src/main/res/values-fr/translations.xml
+++ b/features/ftue/impl/src/main/res/values-fr/translations.xml
@@ -13,10 +13,4 @@
"Vous pourrez modifier vos paramètres ultérieurement.""Autorisez les notifications et ne manquez aucun message""Utiliser la clé de récupération"
- "Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."
- "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."
- "N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."
- "C’est parti !"
- "Voici ce que vous devez savoir :"
- "Bienvenue dans %1$s !"
diff --git a/features/ftue/impl/src/main/res/values-hu/translations.xml b/features/ftue/impl/src/main/res/values-hu/translations.xml
index 5dfdedd4b0e..90a2667d857 100644
--- a/features/ftue/impl/src/main/res/values-hu/translations.xml
+++ b/features/ftue/impl/src/main/res/values-hu/translations.xml
@@ -13,10 +13,4 @@
"A beállításokat később is módosíthatja.""Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem""Adja meg a helyreállítási kulcsot"
- "A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."
- "A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."
- "Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."
- "Lássunk neki!"
- "A következőket kell tudnia:"
- "Üdvözli az %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-in/translations.xml b/features/ftue/impl/src/main/res/values-in/translations.xml
index 5325b7f58d8..3b6f878afce 100644
--- a/features/ftue/impl/src/main/res/values-in/translations.xml
+++ b/features/ftue/impl/src/main/res/values-in/translations.xml
@@ -13,10 +13,4 @@
"Anda dapat mengubah pengaturan Anda nanti.""Izinkan pemberitahuan dan jangan pernah melewatkan pesan""Masukkan kunci pemulihan"
- "Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."
- "Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."
- "Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."
- "Ayo!"
- "Berikut adalah yang perlu Anda ketahui:"
- "Selamat datang di %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-it/translations.xml b/features/ftue/impl/src/main/res/values-it/translations.xml
index 3992acd63eb..c8c4766d2fb 100644
--- a/features/ftue/impl/src/main/res/values-it/translations.xml
+++ b/features/ftue/impl/src/main/res/values-it/translations.xml
@@ -13,10 +13,4 @@
"Potrai modificare le tue impostazioni in seguito.""Consenti le notifiche e non perdere mai un messaggio""Inserisci la chiave di recupero"
- "Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."
- "La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."
- "Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."
- "Andiamo!"
- "Ecco cosa c\'è da sapere:"
- "Benvenuti in %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-ka/translations.xml b/features/ftue/impl/src/main/res/values-ka/translations.xml
index f8ae896aba7..7118eaf913f 100644
--- a/features/ftue/impl/src/main/res/values-ka/translations.xml
+++ b/features/ftue/impl/src/main/res/values-ka/translations.xml
@@ -9,10 +9,4 @@
"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით.""ყველა შეტყობინებაზე შეტყობინებების მიღება""შეიყვანეთ აღდგენის გასაღები"
- "ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."
- "დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი."
- "ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე."
- "დავიწყოთ!"
- "აი, რა უნდა იცოდეთ:"
- "კეთილი იყოს თქვენი მობრძანება %1$s-ში!"
diff --git a/features/ftue/impl/src/main/res/values-ko/translations.xml b/features/ftue/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..cb7c9e32dc9
--- /dev/null
+++ b/features/ftue/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,16 @@
+
+
+ "확인할 수 없나요?"
+ "새로운 복구 키 만들기"
+ "보안 메시징을 설정하려면 이 장치를 확인하세요."
+ "본인 확인"
+ "다른 기기 사용"
+ "복구 키 사용"
+ "이제 메시지를 안전하게 읽거나 보낼 수 있으며, 채팅 상대도 이 기기를 신뢰할 수 있습니다."
+ "기기 검증됨"
+ "다른 기기 사용"
+ "다른 기기에서 대기 중…"
+ "나중에 설정을 변경할 수 있습니다."
+ "알림을 허용하고 메시지를 놓치지 마세요."
+ "복구 키를 입력하세요"
+
diff --git a/features/ftue/impl/src/main/res/values-nb/translations.xml b/features/ftue/impl/src/main/res/values-nb/translations.xml
index 61810952ba0..aa0f4a14430 100644
--- a/features/ftue/impl/src/main/res/values-nb/translations.xml
+++ b/features/ftue/impl/src/main/res/values-nb/translations.xml
@@ -13,10 +13,4 @@
"Du kan endre innstillingene dine senere.""Tillat varslinger og gå aldri glipp av en melding""Skriv inn gjenopprettingsnøkkel"
- "Samtaler, avstemninger, søk og mer vil bli lagt til senere i år."
- "Meldingshistorikk for krypterte rom er ikke tilgjengelig ennå."
- "Vi vil gjerne høre fra deg, så la oss få vite hva du synes via innstillingssiden."
- "Kom igjen!"
- "Her er hva du trenger å vite:"
- "Velkommen til %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-nl/translations.xml b/features/ftue/impl/src/main/res/values-nl/translations.xml
index 459b003e53c..ca49c620afe 100644
--- a/features/ftue/impl/src/main/res/values-nl/translations.xml
+++ b/features/ftue/impl/src/main/res/values-nl/translations.xml
@@ -13,10 +13,4 @@
"Je kunt je instellingen later wijzigen.""Sta meldingen toe en mis nooit meer een bericht""Voer herstelsleutel in"
- "Oproepen, peilingen, zoekopdrachten en meer zullen later dit jaar worden toegevoegd."
- "Berichtgeschiedenis voor versleutelde kamers is nog niet beschikbaar."
- "We horen graag van je, laat ons weten wat je ervan vindt via de instellingenpagina."
- "Aan de slag!"
- "Dit is wat je moet weten:"
- "Welkom bij %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-pl/translations.xml b/features/ftue/impl/src/main/res/values-pl/translations.xml
index 682bae9cf91..c348c9b4f5d 100644
--- a/features/ftue/impl/src/main/res/values-pl/translations.xml
+++ b/features/ftue/impl/src/main/res/values-pl/translations.xml
@@ -13,10 +13,4 @@
"Możesz zmienić ustawienia później.""Zezwól na powiadomienia i nie przegap żadnej wiadomości""Wprowadź klucz przywracania"
- "Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku."
- "Historia wiadomości dla pokoi szyfrowanych nie jest jeszcze dostępna."
- "Chętnie poznamy Twoją opinię. Daj nam znać, co myślisz na stronie ustawień."
- "Naprzód!"
- "Oto, co musisz wiedzieć:"
- "Witamy w %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml b/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml
index 223ceedd31c..8629bbfd119 100644
--- a/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml
@@ -2,21 +2,15 @@
"Não consegue confirmar?""Criar uma nova chave de recuperação"
- "Verifique este dispositivo para configurar mensagens seguras."
+ "Verifique este dispositivo para configurar as mensagens seguras.""Confirme sua identidade""Usar outro dispositivo"
- "Use a chave de recuperação"
+ "Usar chave de recuperação""Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo.""Dispositivo verificado""Usar outro dispositivo"
- "Aguardando outro dispositivo…"
+ "Aguardando o outro dispositivo…""Você pode alterar suas configurações mais tarde."
- "Permita notificações e nunca perca uma mensagem"
- "Insira a chave de recuperação"
- "Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano."
- "O histórico de mensagens para salas criptografadas ainda não está disponível."
- "Adoraríamos ouvir sua opinião. Deixe-nos saber o que você pensa através da página de configurações."
- "Vamos lá!"
- "Aqui está o que você precisa saber:"
- "Bem-vindo ao %1$s!"
+ "Permita as notificações e nunca perca uma mensagem"
+ "Digitar chave de recuperação"
diff --git a/features/ftue/impl/src/main/res/values-pt/translations.xml b/features/ftue/impl/src/main/res/values-pt/translations.xml
index d96caa5065c..c05eca2af5b 100644
--- a/features/ftue/impl/src/main/res/values-pt/translations.xml
+++ b/features/ftue/impl/src/main/res/values-pt/translations.xml
@@ -13,10 +13,4 @@
"Podes alterar as tuas definições mais tarde.""Permite as notificações e nunca percas uma mensagem""Insere a chave de recuperação"
- "Chamadas, sondagens, pesquisa e mais funcionalidades vão ser adicionadas ao longo do ano."
- "O histórico de mensagens em salas cifradas ainda não está disponível."
- "Gostaríamos de ouvir a tua opinião, diz-nos o que pensas através da página de configurações."
- "Vamos lá!"
- "Eis o que tens de saber:"
- "Bem-vindo à %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-ro/translations.xml b/features/ftue/impl/src/main/res/values-ro/translations.xml
index 4d2c9febaeb..b58414a2757 100644
--- a/features/ftue/impl/src/main/res/values-ro/translations.xml
+++ b/features/ftue/impl/src/main/res/values-ro/translations.xml
@@ -13,10 +13,4 @@
"Puteți modifica setările mai târziu.""Permiteți notificările și nu pierdeți niciodată un mesaj""Introduceți cheia de recuperare"
- "Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."
- "Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."
- "Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."
- "Să începem!"
- "Iată ce trebuie să știți:"
- "Bun venit la%1$s!"
diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml
index d287e656407..75a927e927f 100644
--- a/features/ftue/impl/src/main/res/values-ru/translations.xml
+++ b/features/ftue/impl/src/main/res/values-ru/translations.xml
@@ -13,10 +13,4 @@
"Вы можете изменить настройки позже.""Разрешите отправку уведомлений и ни одно сообщение не будет пропущено""Введите ключ восстановления"
- "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."
- "История сообщений для зашифрованных комнат в этом обновлении будет недоступна."
- "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."
- "Поехали!"
- "Вот что вам необходимо знать:"
- "Добро пожаловать в %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml
index 9f39ca6065e..ca2cc3ec630 100644
--- a/features/ftue/impl/src/main/res/values-sk/translations.xml
+++ b/features/ftue/impl/src/main/res/values-sk/translations.xml
@@ -13,10 +13,4 @@
"Svoje nastavenia môžete neskôr zmeniť.""Povoľte oznámenia a nikdy nezmeškajte žiadnu správu""Zadajte kľúč na obnovenie"
- "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."
- "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."
- "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."
- "Poďme na to!"
- "Toto by ste mali vedieť:"
- "Vitajte v %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-sv/translations.xml b/features/ftue/impl/src/main/res/values-sv/translations.xml
index ffaf682393f..0bca53cf1dd 100644
--- a/features/ftue/impl/src/main/res/values-sv/translations.xml
+++ b/features/ftue/impl/src/main/res/values-sv/translations.xml
@@ -13,10 +13,4 @@
"Du kan ändra dina inställningar senare.""Tillåt aviseringar och missa aldrig ett meddelande""Ange återställningsnyckel"
- "Samtal, omröstningar, sökning och mer kommer att läggas till senare i år."
- "Meddelandehistorik för krypterade rum är inte tillgänglig än."
- "Vi vill gärna höra från dig, låt oss veta vad du tycker via inställningssidan."
- "Nu kör vi!"
- "Här är vad du behöver veta:"
- "Välkommen till %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-tr/translations.xml b/features/ftue/impl/src/main/res/values-tr/translations.xml
index dbeb65832cc..dbdfe557f1f 100644
--- a/features/ftue/impl/src/main/res/values-tr/translations.xml
+++ b/features/ftue/impl/src/main/res/values-tr/translations.xml
@@ -13,10 +13,4 @@
"Ayarlarınızı daha sonra değiştirebilirsiniz.""Bildirimlere izin verin ve hiçbir mesajı kaçırmayın""Kurtarma anahtarını girin"
- "Çağrılar, anketler, arama ve daha fazlası bu yıl içinde eklenecek."
- "Şifrelenmiş odalar için mesaj geçmişi henüz mevcut değil."
- "Düşüncelerinizi bizimle paylaşmanızı çok isteriz. Ayarlar sayfasından düşüncelerinizi bize iletin."
- "Hadi başlayalım!"
- "İşte bilmeniz gerekenler:"
- "%1$s hoş geldiniz!"
diff --git a/features/ftue/impl/src/main/res/values-uk/translations.xml b/features/ftue/impl/src/main/res/values-uk/translations.xml
index 6dc00561499..14ee57af8af 100644
--- a/features/ftue/impl/src/main/res/values-uk/translations.xml
+++ b/features/ftue/impl/src/main/res/values-uk/translations.xml
@@ -13,10 +13,4 @@
"Ви можете змінити свої налаштування пізніше.""Дозволити сповіщення і ніколи не пропускати повідомлення""Введіть ключ відновлення"
- "Виклики, опитування, пошук тощо будуть додані пізніше цього року."
- "Історія повідомлень для зашифрованих кімнат ще недоступна."
- "Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."
- "Уперед!"
- "Ось що вам потрібно знати:"
- "Ласкаво просимо до %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-ur/translations.xml b/features/ftue/impl/src/main/res/values-ur/translations.xml
index 69882e87d0f..402140ed71d 100644
--- a/features/ftue/impl/src/main/res/values-ur/translations.xml
+++ b/features/ftue/impl/src/main/res/values-ur/translations.xml
@@ -13,10 +13,4 @@
"آپ بعد میں اپنی ترتیبات تبدیل کر سکتے ہیں۔""اطلاعات کی اجازت دیں اور کبھی بھی کسی پیغام سے محروم نہ ہوں۔""بازیابی کلید درج کریں"
- "مکالمات، رائے شماری، تلاش اور مزید بعد میں اس سال شامل کیا جائے گا۔"
- "مرموز کردہ کمروں کیلئے سرگزشتِ پیغام ابھی دستیاب نہیں ہے"
- "ہم آپ سے سننا پسند کریں گے، ترتیبات کے صفحہ کے ذریعے ہمیں بتائیں کہ آپ کیا سوچتے ہیں۔"
- "چلیں!"
- "یہ ہے جو آپ کو جاننے کی ضرورت ہے:"
- "%1$s میں خوش آمدید!"
diff --git a/features/ftue/impl/src/main/res/values-uz/translations.xml b/features/ftue/impl/src/main/res/values-uz/translations.xml
index f8ede2ecf4d..9ed2a2b86d6 100644
--- a/features/ftue/impl/src/main/res/values-uz/translations.xml
+++ b/features/ftue/impl/src/main/res/values-uz/translations.xml
@@ -1,11 +1,16 @@
+ "Tasdiqlay olmayapsizmi?"
+ "Yangi tiklash kalitini yarating"
+ "Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang."
+ "Shaxsingizni tasdiqlang"
+ "Boshqa qurilmadan foydalanish"
+ "Qayta tiklash kalitidan foydalaning"
+ "Endi xabarlarni xavfsiz tarzda o‘qish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin."
+ "Qurilma tasdiqlandi"
+ "Boshqa qurilmadan foydalanish"
+ "Boshqa qurilmada kutilmoqda…""Sozlamalaringizni keyinroq o\'zgartirishingiz mumkin.""Bildirishnomalarga ruxsat bering va hech qachon xabarni o\'tkazib yubormang"
- "Qo\'ng\'iroqlar, so\'ro\'vlar, qidiruv va boshqalar shu yil oxirida qo\'shiladi."
- "Shifrlangan xonalar uchun xabarlar tarixi hali mavjud emas."
- "Biz sizdan eshitishni istardik, sozlamalar sahifasi orqali fikringizni bildiring."
- "Qani ketdik!"
- "Buni bilishingiz kerak:"
- "%1$sga Xush kelibsiz!"
+ "Tiklash kalitini kiriting"
diff --git a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml
index 5658f306160..93ef03932b0 100644
--- a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml
@@ -13,10 +13,4 @@
"您稍後仍可變更設定。""允許通知,永遠不會錯誤任何訊息""輸入復原金鑰"
- "通話、投票、搜尋等更多功能將在今年登場。"
- "在這次的更新,您無法查看聊天室內被加密的歷史訊息。"
- "我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"
- "開始吧!"
- "我們有些事想告訴您:"
- "歡迎使用 %1$s!"
diff --git a/features/ftue/impl/src/main/res/values-zh/translations.xml b/features/ftue/impl/src/main/res/values-zh/translations.xml
index 88b8b4252e7..1c72ad8c305 100644
--- a/features/ftue/impl/src/main/res/values-zh/translations.xml
+++ b/features/ftue/impl/src/main/res/values-zh/translations.xml
@@ -13,10 +13,4 @@
"您可以稍后更改设置。""允许通知,绝不错过任何消息""输入恢复密钥"
- "今年晚些时候将增加通话、投票、搜索等功能。"
- "加密聊天室的消息历史记录尚不可用。"
- "我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。"
- "开始吧!"
- "以下是您需要了解的内容:"
- "欢迎使用 %1$s"
diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml
index 47214a93041..91cf96df2a5 100644
--- a/features/ftue/impl/src/main/res/values/localazy.xml
+++ b/features/ftue/impl/src/main/res/values/localazy.xml
@@ -13,10 +13,4 @@
"You can change your settings later.""Allow notifications and never miss a message""Enter recovery key"
- "Calls, polls, search and more will be added later this year."
- "Message history for encrypted rooms isn’t available yet."
- "We’d love to hear from you, let us know what you think via the settings page."
- "Let\'s go!"
- "Here’s what you need to know:"
- "Welcome to %1$s!"
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.kt
new file mode 100644
index 00000000000..bbab0fc647d
--- /dev/null
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPointTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.ftue.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.test.FakeLockScreenEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultFtueEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder`() = runTest {
+ val entryPoint = DefaultFtueEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ FtueFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ analyticsEntryPoint = { _, _ -> lambdaError() },
+ defaultFtueService = createDefaultFtueService(),
+ lockScreenEntryPoint = FakeLockScreenEntryPoint(),
+ )
+ }
+ val result = entryPoint.createNode(parentNode, BuildContext.root(null))
+ assertThat(result).isInstanceOf(FtueFlowNode::class.java)
+ }
+}
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
index 4a05433e5e3..dee8c7c091c 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep
+import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -26,8 +28,6 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
-import io.element.android.tests.testutils.lambda.lambdaRecorder
-import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -69,9 +69,11 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent()
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
- service.updateState()
-
- assertThat(service.state.value).isEqualTo(FtueState.Complete)
+ service.updateFtueStep()
+ service.state.test {
+ assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
+ assertThat(awaitItem()).isEqualTo(FtueState.Complete)
+ }
}
@Test
@@ -90,9 +92,11 @@ class DefaultFtueServiceTest {
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
- service.updateState()
-
- assertThat(service.state.value).isEqualTo(FtueState.Complete)
+ service.updateFtueStep()
+ service.state.test {
+ assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
+ assertThat(awaitItem()).isEqualTo(FtueState.Complete)
+ }
}
@Test
@@ -109,35 +113,30 @@ class DefaultFtueServiceTest {
permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService,
)
- val steps = mutableListOf()
-
- // Session verification
- steps.add(service.getNextStep(steps.lastOrNull()))
- sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
-
- // Notifications opt in
- steps.add(service.getNextStep(steps.lastOrNull()))
- permissionStateProvider.setPermissionGranted()
- // Entering PIN code
- steps.add(service.getNextStep(steps.lastOrNull()))
- lockScreenService.setIsPinSetup(true)
-
- // Analytics opt in
- steps.add(service.getNextStep(steps.lastOrNull()))
- analyticsService.setDidAskUserConsent()
-
- // Final step (null)
- steps.add(service.getNextStep(steps.lastOrNull()))
-
- assertThat(steps).containsExactly(
- FtueStep.SessionVerification,
- FtueStep.NotificationsOptIn,
- FtueStep.LockscreenSetup,
- FtueStep.AnalyticsOptIn,
- // Final state
- null,
- )
+ service.ftueStepStateFlow.test {
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
+ // Session verification
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.SessionVerification))
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
+ // User completes verification
+ service.onUserCompletedSessionVerification()
+ // Notifications opt in
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.NotificationsOptIn))
+ permissionStateProvider.setPermissionGranted()
+ // Simulate event from NotificationsOptInNode.Callback.onNotificationsOptInFinished
+ service.updateFtueStep()
+ // Entering PIN code
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.LockscreenSetup))
+ lockScreenService.setIsPinSetup(true)
+ // Simulate event from LockScreenEntryPoint.Callback.onSetupDone()
+ service.updateFtueStep()
+ // Analytics opt in
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
+ analyticsService.setDidAskUserConsent()
+ // Final step
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
+ }
}
@Test
@@ -158,10 +157,13 @@ class DefaultFtueServiceTest {
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
- assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
-
- analyticsService.setDidAskUserConsent()
- assertThat(service.getNextStep(null)).isNull()
+ service.ftueStepStateFlow.test {
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
+ // Analytics opt in
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
+ analyticsService.setDidAskUserConsent()
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
+ }
}
@Test
@@ -180,68 +182,30 @@ class DefaultFtueServiceTest {
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
lockScreenService.setIsPinSetup(true)
- assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
-
- analyticsService.setDidAskUserConsent()
- assertThat(service.getNextStep(null)).isNull()
- }
-
- @Test
- fun `reset do the expected actions S`() = runTest {
- val resetAnalyticsLambda = lambdaRecorder { }
- val analyticsService = FakeAnalyticsService(
- resetLambda = resetAnalyticsLambda
- )
- val resetPermissionLambda = lambdaRecorder { }
- val permissionStateProvider = FakePermissionStateProvider(
- resetPermissionLambda = resetPermissionLambda
- )
- val service = createDefaultFtueService(
- sdkIntVersion = Build.VERSION_CODES.S,
- permissionStateProvider = permissionStateProvider,
- analyticsService = analyticsService,
- )
- service.reset()
- resetAnalyticsLambda.assertions().isCalledOnce()
- resetPermissionLambda.assertions().isNeverCalled()
- }
-
- @Test
- fun `reset do the expected actions TIRAMISU`() = runTest {
- val resetLambda = lambdaRecorder { }
- val analyticsService = FakeAnalyticsService(
- resetLambda = resetLambda
- )
- val resetPermissionLambda = lambdaRecorder { }
- val permissionStateProvider = FakePermissionStateProvider(
- resetPermissionLambda = resetPermissionLambda
- )
- val service = createDefaultFtueService(
- sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
- permissionStateProvider = permissionStateProvider,
- analyticsService = analyticsService,
- )
- service.reset()
- resetLambda.assertions().isCalledOnce()
- resetPermissionLambda.assertions().isCalledOnce()
- .with(value("android.permission.POST_NOTIFICATIONS"))
+ service.ftueStepStateFlow.test {
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
+ // Analytics opt in
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
+ analyticsService.setDidAskUserConsent()
+ assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
+ }
}
-
- private fun TestScope.createDefaultFtueService(
- sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
- analyticsService: AnalyticsService = FakeAnalyticsService(),
- permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
- lockScreenService: LockScreenService = FakeLockScreenService(),
- sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
- // First version where notification permission is required
- sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
- ) = DefaultFtueService(
- sessionCoroutineScope = backgroundScope,
- sessionVerificationService = sessionVerificationService,
- sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
- analyticsService = analyticsService,
- permissionStateProvider = permissionStateProvider,
- lockScreenService = lockScreenService,
- sessionPreferencesStore = sessionPreferencesStore,
- )
}
+
+internal fun TestScope.createDefaultFtueService(
+ sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
+ analyticsService: AnalyticsService = FakeAnalyticsService(),
+ permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
+ lockScreenService: LockScreenService = FakeLockScreenService(),
+ sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
+ // First version where notification permission is required
+ sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
+) = DefaultFtueService(
+ sessionCoroutineScope = backgroundScope,
+ sessionVerificationService = sessionVerificationService,
+ sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
+ analyticsService = analyticsService,
+ permissionStateProvider = permissionStateProvider,
+ lockScreenService = lockScreenService,
+ sessionPreferencesStore = sessionPreferencesStore,
+)
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
index fbdc9d1dd17..32d68dd1e10 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
index 3801001ed41..c95c455bfd3 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
@@ -22,23 +24,92 @@ import org.junit.Test
class ChooseSessionVerificationModePresenterTest {
@Test
- fun `initial state - is relayed from EncryptionService`() = runTest {
- val encryptionService = FakeEncryptionService().apply {
- // Is last device
- emitIsLastDevice(true)
- // Can enter recovery key
- emitRecoveryState(RecoveryState.INCOMPLETE)
- }
- val presenter = createPresenter(encryptionService = encryptionService)
+ fun `present - initial state`() = runTest {
+ val presenter = createPresenter()
presenter.test {
awaitItem().run {
- assertThat(isLastDevice).isTrue()
- assertThat(canEnterRecoveryKey).isTrue()
+ assertThat(buttonsState.isLoading()).isTrue()
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
}
}
}
+ @Test
+ fun `present - state is relayed from EncryptionService, order 1`() = runTest {
+ val encryptionService = FakeEncryptionService()
+ val presenter = createPresenter(encryptionService = encryptionService)
+ presenter.test {
+ assertThat(awaitItem().buttonsState.isLoading()).isTrue()
+ // Has device to verify against
+ encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
+ // Can enter recovery key
+ encryptionService.emitRecoveryState(RecoveryState.DISABLED)
+ assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
+ ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = false,
+ canEnterRecoveryKey = false,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - state is relayed from EncryptionService, order 2`() = runTest {
+ val encryptionService = FakeEncryptionService()
+ val presenter = createPresenter(encryptionService = encryptionService)
+ presenter.test {
+ assertThat(awaitItem().buttonsState.isLoading()).isTrue()
+ // Can enter recovery key
+ encryptionService.emitRecoveryState(RecoveryState.DISABLED)
+ // Has device to verify against
+ encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
+ assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
+ ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = false,
+ canEnterRecoveryKey = false,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - can use another device`() = runTest {
+ val encryptionService = FakeEncryptionService()
+ val presenter = createPresenter(encryptionService = encryptionService)
+ presenter.test {
+ assertThat(awaitItem().buttonsState.isLoading()).isTrue()
+ // Can enter recovery key
+ encryptionService.emitRecoveryState(RecoveryState.DISABLED)
+ // Has device to verify against
+ encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(true))
+ assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
+ ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = true,
+ canEnterRecoveryKey = false,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - can enter recovery key`() = runTest {
+ val encryptionService = FakeEncryptionService()
+ val presenter = createPresenter(encryptionService = encryptionService)
+ presenter.test {
+ assertThat(awaitItem().buttonsState.isLoading()).isTrue()
+ // Can enter recovery key
+ encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
+ // Has device to verify against
+ encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
+ assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
+ ChooseSelfVerificationModeState.ButtonsState(
+ canUseAnotherDevice = false,
+ canEnterRecoveryKey = true,
+ )
+ )
+ }
+ }
+
@Test
fun `sing out action triggers a direct logout`() = runTest {
val logoutEventRecorder = lambdaRecorder {}
@@ -49,8 +120,8 @@ class ChooseSessionVerificationModePresenterTest {
presenter.test {
val initial = awaitItem()
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
-
- logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
+ logoutEventRecorder.assertions().isCalledOnce()
+ .with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
}
}
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
index b89e3e42bd9..f201cb6c418 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,6 +13,7 @@ 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.features.ftue.impl.R
+import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.clickOn
@@ -43,7 +45,7 @@ class ChooseSessionVerificationModeViewTest {
fun `clicking on use another device calls the callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
- aChooseSelfVerificationModeState(isLastDevice = false),
+ aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))),
onUseAnotherDevice = callback,
)
rule.clickOn(R.string.screen_identity_use_another_device)
@@ -55,7 +57,7 @@ class ChooseSessionVerificationModeViewTest {
fun `clicking on enter recovery key calls the callback`() {
ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView(
- aChooseSelfVerificationModeState(canEnterRecoveryKey = true),
+ aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canEnterRecoveryKey = true))),
onEnterRecoveryKey = callback,
)
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/InMemoryWelcomeScreenStore.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/InMemoryWelcomeScreenStore.kt
deleted file mode 100644
index 0cd37bef0a6..00000000000
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/InMemoryWelcomeScreenStore.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.ftue.impl.welcome.state
-
-class InMemoryWelcomeScreenStore : WelcomeScreenStore {
- private var isWelcomeScreenNeeded = true
-
- override fun isWelcomeScreenNeeded(): Boolean {
- return isWelcomeScreenNeeded
- }
-
- override fun setWelcomeScreenShown() {
- isWelcomeScreenNeeded = false
- }
-
- override fun reset() {
- isWelcomeScreenNeeded = true
- }
-}
diff --git a/features/ftue/test/build.gradle.kts b/features/ftue/test/build.gradle.kts
index 396c7467b60..4c7dc575353 100644
--- a/features/ftue/test/build.gradle.kts
+++ b/features/ftue/test/build.gradle.kts
@@ -1,9 +1,8 @@
-import extension.setupAnvil
-
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,8 +15,6 @@ android {
namespace = "io.element.android.features.ftue.test"
}
-setupAnvil()
-
dependencies {
implementation(projects.features.ftue.api)
implementation(projects.tests.testutils)
diff --git a/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt
index 9217dbd22c7..963f67c8e67 100644
--- a/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt
+++ b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,18 +10,11 @@ package io.element.android.features.ftue.test
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
-import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeFtueService(
- private val resetLambda: () -> Unit = { lambdaError() },
-) : FtueService {
+class FakeFtueService : FtueService {
override val state: MutableStateFlow = MutableStateFlow(FtueState.Unknown)
- override suspend fun reset() {
- resetLambda()
- }
-
suspend fun emitState(newState: FtueState) {
state.emit(newState)
}
diff --git a/features/home/api/build.gradle.kts b/features/home/api/build.gradle.kts
index c1aee95e670..2e725659c11 100644
--- a/features/home/api/build.gradle.kts
+++ b/features/home/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
index 0f6ee581bba..b3dd8f1ee19 100644
--- a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
+++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,20 +15,19 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface HomeEntryPoint : FeatureEntryPoint {
- fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
- interface NodeBuilder {
- fun callback(callback: Callback): NodeBuilder
- fun build(): Node
- }
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ callback: Callback,
+ ): Node
interface Callback : Plugin {
- fun onRoomClick(roomId: RoomId)
- fun onCreateRoomClick()
- fun onSettingsClick()
- fun onSetUpRecoveryClick()
- fun onSessionConfirmRecoveryKeyClick()
- fun onRoomSettingsClick(roomId: RoomId)
- fun onReportBugClick()
- fun onLogoutForNativeSlidingSyncMigrationNeeded()
+ fun navigateToRoom(roomId: RoomId)
+ fun navigateToCreateRoom()
+ fun navigateToSettings()
+ fun navigateToSetUpRecovery()
+ fun navigateToEnterRecoveryKey()
+ fun navigateToRoomSettings(roomId: RoomId)
+ fun navigateToBugReport()
}
}
diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts
index 88184bcbcd9..b36ee6aed2a 100644
--- a/features/home/impl/build.gradle.kts
+++ b/features/home/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,7 +24,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
@@ -38,12 +40,13 @@ dependencies {
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.indicator.api)
- implementation(projects.libraries.deeplink)
+ implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.fullscreenintent.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
+ implementation(projects.features.announcement.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
@@ -54,16 +57,12 @@ dependencies {
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(projects.features.reportroom.api)
+ implementation(projects.features.rolesandpermissions.api)
+ implementation(projects.libraries.previewutils)
api(projects.features.home.api)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
+ testCommonDependencies(libs, true)
+ testImplementation(projects.features.announcement.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.networkmonitor.test)
@@ -75,8 +74,8 @@ dependencies {
testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
+ testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
- testImplementation(projects.tests.testutils)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt
new file mode 100644
index 00000000000..b29dc788a78
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilder.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl
+
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.sessionstorage.api.SessionData
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+class CurrentUserWithNeighborsBuilder {
+ /**
+ * Build a list of [MatrixUser] containing the current user. If there are other sessions, the list
+ * will contain 3 users, with the current user in the middle.
+ * If there is only one other session, the list will contain twice the other user, to allow cycling.
+ */
+ fun build(
+ matrixUser: MatrixUser,
+ sessions: List,
+ ): ImmutableList {
+ // Sort by position to always have the same order (not depending on last account usage)
+ return sessions.sortedBy { it.position }
+ .map {
+ if (it.userId == matrixUser.userId.value) {
+ // Always use the freshest profile for the current user
+ matrixUser
+ } else {
+ // Use the data from the DB
+ MatrixUser(
+ userId = UserId(it.userId),
+ displayName = it.userDisplayName,
+ avatarUrl = it.userAvatarUrl,
+ )
+ }
+ }
+ .let { sessionList ->
+ // If the list has one item, there is no other session, return the list
+ when (sessionList.size) {
+ // Can happen when the user signs out (?)
+ 0 -> listOf(matrixUser)
+ 1 -> sessionList
+ else -> {
+ // Create a list with extra item at the start and end if necessary to have the current user in the middle
+ // If the list is [A, B, C, D] and the current user is A we want to return [D, A, B]
+ // If the current user is B, we want to return [A, B, C]
+ // If the current user is C, we want to return [B, C, D]
+ // If the current user is D, we want to return [C, D, A]
+ // Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling
+ // between the two users.
+ val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId }
+ when (currentUserIndex) {
+ // This can happen when the user signs out.
+ // In this case, just return a singleton list with the current user.
+ -1 -> listOf(matrixUser)
+ 0 -> listOf(sessionList.last()) + sessionList.take(2)
+ sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first()
+ else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1)
+ }
+ }
+ }
+ }
+ .toImmutableList()
+ }
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt
index 3fe5c533d74..8da31b601d5 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,27 +10,18 @@ package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultHomeEntryPoint @Inject constructor() : HomeEntryPoint {
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): HomeEntryPoint.NodeBuilder {
- val plugins = ArrayList()
-
- return object : HomeEntryPoint.NodeBuilder {
- override fun callback(callback: HomeEntryPoint.Callback): HomeEntryPoint.NodeBuilder {
- plugins += callback
- return this
- }
-
- override fun build(): Node {
- return parentNode.createNode(buildContext, plugins)
- }
- }
+class DefaultHomeEntryPoint : HomeEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ callback: HomeEntryPoint.Callback,
+ ): Node {
+ return parentNode.createNode(buildContext, listOf(callback))
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt
index 4632e40d5a9..db9dafba63d 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt
@@ -1,12 +1,16 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
+import io.element.android.libraries.matrix.api.core.SessionId
+
sealed interface HomeEvents {
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
+ data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
index 4a7e5eb258c..78e48fad791 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,39 +12,56 @@ import android.app.Activity
import android.os.Parcelable
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
import com.bumble.appyx.core.lifecycle.subscribe
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 com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.model.RoomListRoomSummary
+import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
+import io.element.android.features.leaveroom.api.LeaveRoomRenderer
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
+import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
+import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
-import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
+import io.element.android.libraries.architecture.appyx.launchMolecule
+import io.element.android.libraries.architecture.callback
+import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
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 io.element.android.services.analytics.api.AnalyticsService
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class HomeFlowNode @AssistedInject constructor(
+@AssistedInject
+class HomeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
+ private val matrixClient: MatrixClient,
private val presenter: HomePresenter,
private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService,
@@ -51,6 +69,8 @@ class HomeFlowNode @AssistedInject constructor(
private val directLogoutView: DirectLogoutView,
private val reportRoomEntryPoint: ReportRoomEntryPoint,
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
+ private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
+ private val leaveRoomRenderer: LeaveRoomRenderer,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -59,12 +79,30 @@ class HomeFlowNode @AssistedInject constructor(
buildContext = buildContext,
plugins = plugins
) {
- init {
+ private val callback: HomeEntryPoint.Callback = callback()
+ private val stateFlow = launchMolecule { presenter.present() }
+
+ override fun onBuilt() {
+ super.onBuilt()
lifecycle.subscribe(
onResume = {
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home))
}
)
+ whenChildAttached {
+ commonLifecycle: Lifecycle,
+ changeRoomMemberRolesNode: ChangeRoomMemberRolesEntryPoint.NodeProxy,
+ ->
+ commonLifecycle.coroutineScope.launch {
+ val isNewOwnerSelected = changeRoomMemberRolesNode.waitForCompletion()
+ withContext(NonCancellable) {
+ backstack.pop()
+ if (isNewOwnerSelected) {
+ onNewOwnersSelected(changeRoomMemberRolesNode.roomId)
+ }
+ }
+ }
+ }
}
sealed interface NavTarget : Parcelable {
@@ -76,37 +114,16 @@ class HomeFlowNode @AssistedInject constructor(
@Parcelize
data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget
- }
-
- private fun onRoomClick(roomId: RoomId) {
- plugins().forEach { it.onRoomClick(roomId) }
- }
-
- private fun onOpenSettings() {
- plugins().forEach { it.onSettingsClick() }
- }
- private fun onCreateRoomClick() {
- plugins().forEach { it.onCreateRoomClick() }
- }
-
- private fun onSetUpRecoveryClick() {
- plugins().forEach { it.onSetUpRecoveryClick() }
- }
-
- private fun onSessionConfirmRecoveryKeyClick() {
- plugins().forEach { it.onSessionConfirmRecoveryKeyClick() }
- }
-
- private fun onRoomSettingsClick(roomId: RoomId) {
- plugins().forEach { it.onRoomSettingsClick(roomId) }
+ @Parcelize
+ data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget
}
- private fun onReportRoomClick(roomId: RoomId) {
+ private fun navigateToReportRoom(roomId: RoomId) {
backstack.push(NavTarget.ReportRoom(roomId))
}
- private fun onDeclineInviteAndBlockUserClick(roomSummary: RoomListRoomSummary) {
+ private fun navigateToDeclineInviteAndBlockUser(roomSummary: RoomListRoomSummary) {
backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData()))
}
@@ -116,37 +133,51 @@ class HomeFlowNode @AssistedInject constructor(
inviteFriendsUseCase.execute(activity)
}
RoomListMenuAction.ReportBug -> {
- plugins().forEach { it.onReportBugClick() }
+ callback.navigateToBugReport()
}
}
}
- fun rootNode(buildContext: BuildContext): Node {
+ private fun navigateToSelectNewOwnersWhenLeavingRoom(roomId: RoomId) {
+ backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId))
+ }
+
+ private fun onNewOwnersSelected(roomId: RoomId) {
+ stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
+ }
+
+ private fun rootNode(buildContext: BuildContext): Node {
return node(buildContext) { modifier ->
- val state = presenter.present()
+ val state by stateFlow.collectAsState()
val activity = requireNotNull(LocalActivity.current)
-
HomeView(
homeState = state,
- onRoomClick = this::onRoomClick,
- onSettingsClick = this::onOpenSettings,
- onCreateRoomClick = this::onCreateRoomClick,
- onSetUpRecoveryClick = this::onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
- onRoomSettingsClick = this::onRoomSettingsClick,
+ onRoomClick = callback::navigateToRoom,
+ onSettingsClick = callback::navigateToSettings,
+ onStartChatClick = callback::navigateToCreateRoom,
+ onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
+ onConfirmRecoveryKeyClick = callback::navigateToEnterRecoveryKey,
+ onRoomSettingsClick = callback::navigateToRoomSettings,
onMenuActionClick = { onMenuActionClick(activity, it) },
- onReportRoomClick = this::onReportRoomClick,
- onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick,
+ onReportRoomClick = ::navigateToReportRoom,
+ onDeclineInviteAndBlockUser = ::navigateToDeclineInviteAndBlockUser,
modifier = modifier,
- ) {
- acceptDeclineInviteView.Render(
- state = state.roomListState.acceptDeclineInviteState,
- onAcceptInviteSuccess = this::onRoomClick,
- onDeclineInviteSuccess = { },
- modifier = Modifier
- )
- }
-
+ acceptDeclineInviteView = {
+ acceptDeclineInviteView.Render(
+ state = state.roomListState.acceptDeclineInviteState,
+ onAcceptInviteSuccess = callback::navigateToRoom,
+ onDeclineInviteSuccess = { },
+ modifier = Modifier
+ )
+ },
+ leaveRoomView = {
+ leaveRoomRenderer.Render(
+ state = state.roomListState.leaveRoomState,
+ onSelectNewOwners = ::navigateToSelectNewOwnersWhenLeavingRoom,
+ modifier = Modifier
+ )
+ }
+ )
directLogoutView.Render(state.directLogoutState)
}
}
@@ -158,8 +189,29 @@ class HomeFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
- is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId)
- is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData)
+ is NavTarget.ReportRoom -> {
+ reportRoomEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ roomId = navTarget.roomId,
+ )
+ }
+ is NavTarget.DeclineInviteAndBlockUser -> {
+ declineInviteAndBlockUserEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ inviteData = navTarget.inviteData,
+ )
+ }
+ is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
+ val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found")
+ changeRoomMemberRolesEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ room = room,
+ listType = ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving,
+ )
+ }
NavTarget.Root -> rootNode(buildContext)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
index 5254648f24e..6a4a5e16889 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,18 +17,18 @@ enum class HomeNavigationBarItem(
val labelRes: Int,
) {
Chats(
- labelRes = R.string.screen_roomlist_main_space_title
+ labelRes = R.string.screen_home_tab_chats
),
Spaces(
- // TODO Create a new entry in Localazy
- labelRes = R.string.screen_roomlist_main_space_title
+ labelRes = R.string.screen_home_tab_spaces
);
@Composable
- fun icon() = when (this) {
- Chats -> CompoundIcons.ChatSolid()
- // TODO Spaces -> CompoundIcons.Workspace()
- Spaces -> CompoundIcons.Code()
+ fun icon(
+ isSelected: Boolean,
+ ) = when (this) {
+ Chats -> if (isSelected) CompoundIcons.ChatSolid() else CompoundIcons.Chat()
+ Spaces -> if (isSelected) CompoundIcons.WorkspaceSolid() else CompoundIcons.Workspace()
}
companion object {
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt
index 894ed29e8d8..e53d20857eb 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,9 +15,14 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.RoomListState
+import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
@@ -27,24 +33,42 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
-import javax.inject.Inject
+import io.element.android.libraries.sessionstorage.api.SessionStore
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
-class HomePresenter @Inject constructor(
+@Inject
+class HomePresenter(
private val client: MatrixClient,
private val syncService: SyncService,
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter,
+ private val homeSpacesPresenter: Presenter,
private val logoutPresenter: Presenter,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
+ private val sessionStore: SessionStore,
+ private val announcementService: AnnouncementService,
) : Presenter {
+ private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
+
@Composable
override fun present(): HomeState {
- val matrixUser = client.userProfile.collectAsState()
+ val coroutineState = rememberCoroutineScope()
+ val matrixUser by client.userProfile.collectAsState()
+ val currentUserAndNeighbors by remember {
+ combine(
+ client.userProfile,
+ sessionStore.sessionsFlow(),
+ currentUserWithNeighborsBuilder::build,
+ )
+ }.collectAsState(initial = persistentListOf(matrixUser))
val isOnline by syncService.isOnline.collectAsState()
- val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
+ val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val roomListState = roomListPresenter.present()
+ val homeSpacesState = homeSpacesPresenter.present()
val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
}.collectAsState(initial = false)
@@ -62,26 +86,39 @@ class HomePresenter @Inject constructor(
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present()
- fun handleEvents(event: HomeEvents) {
+ fun handleEvent(event: HomeEvents) {
when (event) {
- is HomeEvents.SelectHomeNavigationBarItem -> {
+ is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch {
+ if (event.item == HomeNavigationBarItem.Spaces) {
+ announcementService.showAnnouncement(Announcement.Space)
+ }
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
+ is HomeEvents.SwitchToAccount -> coroutineState.launch {
+ sessionStore.setLatestSession(event.sessionId.value)
+ }
}
}
+ LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) {
+ // If the last space is left, ensure that the Chat view is rendered.
+ if (homeSpacesState.spaceRooms.isEmpty()) {
+ currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
+ }
+ }
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
- matrixUser = matrixUser.value,
+ currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
+ homeSpacesState = homeSpacesState,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt
index 5e6c16d2e42..90667a87341 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt
@@ -1,25 +1,31 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
-import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.roomlist.RoomListState
+import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
+import kotlinx.collections.immutable.ImmutableList
-@Immutable
data class HomeState(
- val matrixUser: MatrixUser,
+ /**
+ * The current user of this session, in case of multiple accounts, will contains 3 items, with the
+ * current user in the middle.
+ */
+ val currentUserAndNeighbors: ImmutableList,
val showAvatarIndicator: Boolean,
val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,
val roomListState: RoomListState,
+ val homeSpacesState: HomeSpacesState,
val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,
@@ -27,4 +33,6 @@ data class HomeState(
val eventSink: (HomeEvents) -> Unit,
) {
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
+ val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
+ val showNavigationBar = isSpaceFeatureEnabled && homeSpacesState.spaceRooms.isNotEmpty()
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt
index 7a50296e170..43010b17202 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,12 +14,15 @@ import io.element.android.features.home.impl.roomlist.RoomListStateProvider
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
+import io.element.android.features.home.impl.spaces.HomeSpacesState
+import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.toImmutableList
open class HomeStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -34,6 +38,8 @@ open class HomeStateProvider : PreviewParameterProvider {
summaries = generateRoomListRoomSummaryList(),
)
),
+ // For the bottom nav bar to be visible in the preview, the user must be member of at least one space
+ homeSpacesState = aHomeSpacesState(),
),
aHomeState(
isSpaceFeatureEnabled = true,
@@ -46,17 +52,19 @@ open class HomeStateProvider : PreviewParameterProvider {
internal fun aHomeState(
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
+ currentUserAndNeighbors: List = listOf(matrixUser),
showAvatarIndicator: Boolean = false,
hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null,
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
roomListState: RoomListState = aRoomListState(),
+ homeSpacesState: HomeSpacesState = aHomeSpacesState(),
canReportBug: Boolean = true,
isSpaceFeatureEnabled: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {}
) = HomeState(
- matrixUser = matrixUser,
+ currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
@@ -64,6 +72,7 @@ internal fun aHomeState(
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
+ homeSpacesState = homeSpacesState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = eventSink,
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
index fb5b9c9ae37..42e4f720735 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,24 +13,25 @@ package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
@@ -39,29 +41,30 @@ import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.home.impl.components.HomeTopBar
import io.element.android.features.home.impl.components.RoomListContentView
import io.element.android.features.home.impl.components.RoomListMenuAction
-import io.element.android.features.home.impl.components.RoomListTopBar
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListContextMenu
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView
-import io.element.android.features.leaveroom.api.LeaveRoomView
-import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
+import io.element.android.features.home.impl.spaces.HomeSpacesView
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
+import io.element.android.libraries.designsystem.theme.components.NavigationBarIcon
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
+import io.element.android.libraries.designsystem.theme.components.NavigationBarText
import io.element.android.libraries.designsystem.theme.components.Scaffold
-import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
+import kotlinx.coroutines.launch
@Composable
fun HomeView(
@@ -70,67 +73,59 @@ fun HomeView(
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
- onCreateRoomClick: () -> Unit,
+ onStartChatClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit,
onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit,
- modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ leaveRoomView: @Composable () -> Unit,
) {
val state: RoomListState = homeState.roomListState
val coroutineScope = rememberCoroutineScope()
val firstThrottler = remember { FirstThrottler(300, coroutineScope) }
-
- ConnectivityIndicatorContainer(
- modifier = modifier,
- isOnline = homeState.hasNetworkConnection,
- ) { topPadding ->
- Box {
- if (state.contextMenu is RoomListState.ContextMenu.Shown) {
- RoomListContextMenu(
- contextMenu = state.contextMenu,
- canReportRoom = state.canReportRoom,
- eventSink = state.eventSink,
- onRoomSettingsClick = onRoomSettingsClick,
- onReportRoomClick = onReportRoomClick,
- )
- }
- if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) {
- RoomListDeclineInviteMenu(
- menu = state.declineInviteMenu,
- canReportRoom = state.canReportRoom,
- eventSink = state.eventSink,
- onDeclineAndBlockClick = onDeclineInviteAndBlockUser,
- )
- }
-
- LeaveRoomView(state = state.leaveRoomState)
-
- HomeScaffold(
- state = homeState,
- onSetUpRecoveryClick = onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
- onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
- onCreateRoomClick = { if (firstThrottler.canHandle()) onCreateRoomClick() },
- onMenuActionClick = onMenuActionClick,
- modifier = Modifier.padding(top = topPadding),
+ Box(modifier) {
+ if (state.contextMenu is RoomListState.ContextMenu.Shown) {
+ RoomListContextMenu(
+ contextMenu = state.contextMenu,
+ canReportRoom = state.canReportRoom,
+ eventSink = state.eventSink,
+ onRoomSettingsClick = onRoomSettingsClick,
+ onReportRoomClick = onReportRoomClick,
)
- // This overlaid view will only be visible when state.displaySearchResults is true
- RoomListSearchView(
- state = state.searchState,
+ }
+ if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) {
+ RoomListDeclineInviteMenu(
+ menu = state.declineInviteMenu,
+ canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
- hideInvitesAvatars = state.hideInvitesAvatars,
- onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
- modifier = Modifier
- .statusBarsPadding()
- .padding(top = topPadding)
- .fillMaxSize()
- .background(ElementTheme.colors.bgCanvasDefault)
+ onDeclineAndBlockClick = onDeclineInviteAndBlockUser,
)
- acceptDeclineInviteView()
}
+
+ leaveRoomView()
+
+ HomeScaffold(
+ state = homeState,
+ onSetUpRecoveryClick = onSetUpRecoveryClick,
+ onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+ onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
+ onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
+ onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
+ onMenuActionClick = onMenuActionClick,
+ )
+ // This overlaid view will only be visible when state.displaySearchResults is true
+ RoomListSearchView(
+ state = state.searchState,
+ eventSink = state.eventSink,
+ hideInvitesAvatars = state.hideInvitesAvatars,
+ onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
+ modifier = Modifier
+ .fillMaxSize()
+ .background(ElementTheme.colors.bgCanvasDefault)
+ )
+ acceptDeclineInviteView()
}
}
@@ -142,7 +137,7 @@ private fun HomeScaffold(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomId) -> Unit,
onOpenSettings: () -> Unit,
- onCreateRoomClick: () -> Unit,
+ onStartChatClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -151,7 +146,7 @@ private fun HomeScaffold(
}
val appBarState = rememberTopAppBarState()
- val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(appBarState)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState
@@ -162,53 +157,67 @@ private fun HomeScaffold(
}
val hazeState = rememberHazeState()
+ val roomsLazyListState = rememberLazyListState()
+ val spacesLazyListState = rememberLazyListState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
- RoomListTopBar(
+ HomeTopBar(
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
- matrixUser = state.matrixUser,
+ currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClick = onMenuActionClick,
onOpenSettings = onOpenSettings,
+ onAccountSwitch = {
+ state.eventSink(HomeEvents.SwitchToAccount(it))
+ },
scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions,
- displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
+ displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
canReportBug = state.canReportBug,
+ modifier = if (state.isSpaceFeatureEnabled) {
+ Modifier.hazeEffect(
+ state = hazeState,
+ style = HazeMaterials.thick(),
+ )
+ } else {
+ Modifier.background(ElementTheme.colors.bgCanvasDefault)
+ }
)
},
bottomBar = {
- if (state.isSpaceFeatureEnabled) {
- NavigationBar(
- containerColor = Color.Transparent,
- modifier = Modifier
- .hazeEffect(
- state = hazeState,
- style = HazeMaterials.regular(),
- )
- ) {
- HomeNavigationBarItem.entries.forEach { item ->
- NavigationBarItem(
- selected = state.currentHomeNavigationBarItem == item,
- onClick = {
- state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
- },
- icon = {
- Icon(
- imageVector = item.icon(),
- contentDescription = null
- )
- },
- label = {
- Text(stringResource(item.labelRes))
+ if (state.showNavigationBar) {
+ val coroutineScope = rememberCoroutineScope()
+ HomeBottomBar(
+ currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
+ onItemClick = { item ->
+ // scroll to top if selecting the same item
+ if (item == state.currentHomeNavigationBarItem) {
+ val lazyListStateTarget = when (item) {
+ HomeNavigationBarItem.Chats -> roomsLazyListState
+ HomeNavigationBarItem.Spaces -> spacesLazyListState
}
- )
- }
- }
+ coroutineScope.launch {
+ if (lazyListStateTarget.firstVisibleItemIndex > 10) {
+ lazyListStateTarget.scrollToItem(10)
+ }
+ // Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls
+ scrollBehavior.state.heightOffset = 0f
+ lazyListStateTarget.animateScrollToItem(0)
+ }
+ } else {
+ state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
+ }
+ },
+ modifier = Modifier.hazeEffect(
+ state = hazeState,
+ style = HazeMaterials.thick(),
+ )
+ )
}
},
content = { padding ->
@@ -217,53 +226,60 @@ private fun HomeScaffold(
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
+ lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
- onCreateRoomClick = onCreateRoomClick,
- // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
- // and include provided bottom padding
- contentBottomPadding = 80.dp + padding.calculateBottomPadding(),
+ onCreateRoomClick = onStartChatClick,
+ contentPadding = PaddingValues(
+ // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
+ // and include provided bottom padding
+ // Disable contentPadding due to navigation issue using the keyboard
+ // See https://issuetracker.google.com/issues/436432313
+ bottom = 80.dp,
+ // bottom = 80.dp + padding.calculateBottomPadding(),
+ // top = padding.calculateTopPadding()
+ ),
modifier = Modifier
.padding(
- top = padding.calculateTopPadding(),
- bottom = 0.dp,
- start = padding.calculateStartPadding(LocalLayoutDirection.current),
- end = padding.calculateEndPadding(LocalLayoutDirection.current),
+ PaddingValues(
+ start = padding.calculateStartPadding(LocalLayoutDirection.current),
+ end = padding.calculateEndPadding(LocalLayoutDirection.current),
+ // Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
+ bottom = padding.calculateBottomPadding(),
+ top = padding.calculateTopPadding()
+ )
)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
)
}
HomeNavigationBarItem.Spaces -> {
- Box(
+ HomeSpacesView(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
- ) {
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "Spaces are coming soon!",
- style = ElementTheme.typography.fontBodyLgRegular,
- color = ElementTheme.colors.textPrimary,
- )
- }
+ .hazeSource(state = hazeState),
+ state = state.homeSpacesState,
+ lazyListState = spacesLazyListState,
+ onSpaceClick = { spaceId ->
+ onRoomClick(spaceId)
+ }
+ )
}
}
},
floatingActionButton = {
if (state.displayActions) {
FloatingActionButton(
- containerColor = ElementTheme.colors.iconPrimary,
- onClick = onCreateRoomClick
+ onClick = onStartChatClick,
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
- tint = ElementTheme.colors.iconOnSolidPrimary,
)
}
}
@@ -272,6 +288,38 @@ private fun HomeScaffold(
)
}
+@Composable
+private fun HomeBottomBar(
+ currentHomeNavigationBarItem: HomeNavigationBarItem,
+ onItemClick: (HomeNavigationBarItem) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavigationBar(
+ containerColor = Color.Transparent,
+ modifier = modifier
+ ) {
+ HomeNavigationBarItem.entries.forEach { item ->
+ val isSelected = currentHomeNavigationBarItem == item
+ NavigationBarItem(
+ selected = isSelected,
+ onClick = {
+ onItemClick(item)
+ },
+ icon = {
+ NavigationBarIcon(
+ imageVector = item.icon(isSelected),
+ )
+ },
+ label = {
+ NavigationBarText(
+ text = stringResource(item.labelRes),
+ )
+ }
+ )
+ }
+ }
+}
+
internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight
@@ -283,11 +331,31 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
- onCreateRoomClick = {},
+ onStartChatClick = {},
+ onRoomSettingsClick = {},
+ onReportRoomClick = {},
+ onMenuActionClick = {},
+ onDeclineInviteAndBlockUser = {},
+ acceptDeclineInviteView = {},
+ leaveRoomView = {}
+ )
+}
+
+@Preview
+@Composable
+internal fun HomeViewA11yPreview() = ElementPreview {
+ HomeView(
+ homeState = aHomeState(),
+ onRoomClick = {},
+ onSettingsClick = {},
+ onSetUpRecoveryClick = {},
+ onConfirmRecoveryKeyClick = {},
+ onStartChatClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},
onDeclineInviteAndBlockUser = {},
acceptDeclineInviteView = {},
+ leaveRoomView = {}
)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt
index 389c2aecfb4..cc3227f965c 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
index 017ca9592d6..c6a9480bc9b 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt
index c833e2914a5..c0f83538c99 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
index e89392da62b..92a5d8dbaa0 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt
new file mode 100644
index 00000000000..093b91fb661
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.pager.VerticalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import io.element.android.appconfig.RoomListConfig
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.home.impl.R
+import io.element.android.features.home.impl.filters.RoomListFiltersState
+import io.element.android.features.home.impl.filters.RoomListFiltersView
+import io.element.android.features.home.impl.filters.aRoomListFiltersState
+import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
+import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
+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.AvatarType
+import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
+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.DropdownMenu
+import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
+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.SessionId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.testtags.TestTags
+import io.element.android.libraries.testtags.testTag
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeTopBar(
+ title: String,
+ currentUserAndNeighbors: ImmutableList,
+ showAvatarIndicator: Boolean,
+ areSearchResultsDisplayed: Boolean,
+ onToggleSearch: () -> Unit,
+ onMenuActionClick: (RoomListMenuAction) -> Unit,
+ onOpenSettings: () -> Unit,
+ onAccountSwitch: (SessionId) -> Unit,
+ scrollBehavior: TopAppBarScrollBehavior,
+ displayMenuItems: Boolean,
+ canReportBug: Boolean,
+ displayFilters: Boolean,
+ filtersState: RoomListFiltersState,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier) {
+ TopAppBar(
+ modifier = Modifier
+ .backgroundVerticalGradient(
+ isVisible = !areSearchResultsDisplayed,
+ )
+ .statusBarsPadding(),
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent,
+ scrolledContainerColor = Color.Transparent,
+ ),
+ title = {
+ Text(
+ modifier = Modifier.semantics {
+ heading()
+ },
+ style = ElementTheme.typography.aliasScreenTitle,
+ text = title,
+ )
+ },
+ navigationIcon = {
+ NavigationIcon(
+ currentUserAndNeighbors = currentUserAndNeighbors,
+ showAvatarIndicator = showAvatarIndicator,
+ onAccountSwitch = onAccountSwitch,
+ onClick = onOpenSettings,
+ )
+ },
+ actions = {
+ if (displayMenuItems) {
+ IconButton(
+ onClick = onToggleSearch,
+ ) {
+ Icon(
+ imageVector = CompoundIcons.Search(),
+ contentDescription = stringResource(CommonStrings.action_search),
+ )
+ }
+ if (RoomListConfig.HAS_DROP_DOWN_MENU) {
+ var showMenu by remember { mutableStateOf(false) }
+ IconButton(
+ onClick = { showMenu = !showMenu }
+ ) {
+ Icon(
+ imageVector = CompoundIcons.OverflowVertical(),
+ contentDescription = null,
+ )
+ }
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false }
+ ) {
+ if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
+ DropdownMenuItem(
+ onClick = {
+ showMenu = false
+ onMenuActionClick(RoomListMenuAction.InviteFriends)
+ },
+ text = { Text(stringResource(id = CommonStrings.action_invite)) },
+ leadingIcon = {
+ Icon(
+ imageVector = CompoundIcons.ShareAndroid(),
+ tint = ElementTheme.colors.iconSecondary,
+ contentDescription = null,
+ )
+ }
+ )
+ }
+ if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
+ DropdownMenuItem(
+ onClick = {
+ showMenu = false
+ onMenuActionClick(RoomListMenuAction.ReportBug)
+ },
+ text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
+ leadingIcon = {
+ Icon(
+ imageVector = CompoundIcons.ChatProblem(),
+ tint = ElementTheme.colors.iconSecondary,
+ contentDescription = null,
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+ },
+ // We want a 16dp left padding for the navigationIcon :
+ // 4dp from default TopAppBarHorizontalPadding
+ // 8dp from AccountIcon default padding (because of IconButton)
+ // 4dp extra padding using left insets
+ windowInsets = WindowInsets(left = 4.dp),
+ )
+ if (displayFilters) {
+ TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
+ RoomListFiltersView(
+ state = filtersState,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun NavigationIcon(
+ currentUserAndNeighbors: ImmutableList,
+ showAvatarIndicator: Boolean,
+ onAccountSwitch: (SessionId) -> Unit,
+ onClick: () -> Unit,
+) {
+ if (currentUserAndNeighbors.size == 1) {
+ AccountIcon(
+ matrixUser = currentUserAndNeighbors.single(),
+ isCurrentAccount = true,
+ showAvatarIndicator = showAvatarIndicator,
+ onClick = onClick,
+ )
+ } else {
+ // Render a vertical pager
+ val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size }
+ // Listen to page changes and switch account if needed
+ val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch)
+ LaunchedEffect(pagerState) {
+ snapshotFlow { pagerState.settledPage }.collect { page ->
+ latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
+ }
+ }
+ VerticalPager(
+ state = pagerState,
+ modifier = Modifier.height(48.dp),
+ ) { page ->
+ AccountIcon(
+ matrixUser = currentUserAndNeighbors[page],
+ isCurrentAccount = page == 1,
+ showAvatarIndicator = page == 1 && showAvatarIndicator,
+ onClick = if (page == 1) {
+ onClick
+ } else {
+ {}
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun AccountIcon(
+ matrixUser: MatrixUser,
+ isCurrentAccount: Boolean,
+ showAvatarIndicator: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
+ IconButton(
+ modifier = modifier.then(testTag),
+ onClick = onClick,
+ ) {
+ Box {
+ val avatarData by remember(matrixUser) {
+ derivedStateOf {
+ matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
+ }
+ }
+ Avatar(
+ avatarData = avatarData,
+ avatarType = AvatarType.User,
+ contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
+ )
+ if (showAvatarIndicator) {
+ RedIndicatorAtom(
+ modifier = Modifier.align(Alignment.TopEnd)
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@PreviewsDayNight
+@Composable
+internal fun HomeTopBarPreview() = ElementPreview {
+ HomeTopBar(
+ title = stringResource(R.string.screen_roomlist_main_space_title),
+ currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
+ showAvatarIndicator = false,
+ areSearchResultsDisplayed = false,
+ scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
+ onOpenSettings = {},
+ onAccountSwitch = {},
+ onToggleSearch = {},
+ displayMenuItems = true,
+ canReportBug = true,
+ displayFilters = true,
+ filtersState = aRoomListFiltersState(),
+ onMenuActionClick = {},
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@PreviewsDayNight
+@Composable
+internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
+ HomeTopBar(
+ title = stringResource(R.string.screen_roomlist_main_space_title),
+ currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
+ showAvatarIndicator = true,
+ areSearchResultsDisplayed = false,
+ scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
+ onOpenSettings = {},
+ onAccountSwitch = {},
+ onToggleSearch = {},
+ displayMenuItems = true,
+ canReportBug = true,
+ displayFilters = true,
+ filtersState = aRoomListFiltersState(),
+ onMenuActionClick = {},
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@PreviewsDayNight
+@Composable
+internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
+ HomeTopBar(
+ title = stringResource(R.string.screen_roomlist_main_space_title),
+ currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
+ showAvatarIndicator = false,
+ areSearchResultsDisplayed = false,
+ scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
+ onOpenSettings = {},
+ onAccountSwitch = {},
+ onToggleSearch = {},
+ displayMenuItems = true,
+ canReportBug = true,
+ displayFilters = true,
+ filtersState = aRoomListFiltersState(),
+ onMenuActionClick = {},
+ )
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.kt
new file mode 100644
index 00000000000..c1c5ac0aef2
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/NewNotificationSoundBanner.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import io.element.android.features.home.impl.R
+import io.element.android.libraries.designsystem.components.Announcement
+import io.element.android.libraries.designsystem.components.AnnouncementType
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@Composable
+internal fun NewNotificationSoundBanner(
+ onDismissClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Announcement(
+ modifier = modifier.roomListBannerPadding(),
+ title = stringResource(R.string.banner_new_sound_title),
+ description = stringResource(R.string.banner_new_sound_message),
+ type = AnnouncementType.Actionable(
+ actionText = stringResource(CommonStrings.action_ok),
+ onActionClick = onDismissClick,
+ onDismissClick = onDismissClick,
+ ),
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun NewNotificationSoundBannerPreview() = ElementPreview {
+ NewNotificationSoundBanner(
+ onDismissClick = {},
+ )
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
index 993fb0b85c1..364fe596f89 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
@@ -31,7 +33,6 @@ 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 androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -61,50 +62,61 @@ import kotlinx.collections.immutable.ImmutableList
fun RoomListContentView(
contentState: RoomListContentState,
filtersState: RoomListFiltersState,
+ lazyListState: LazyListState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
- Box(modifier = modifier) {
- when (contentState) {
- is RoomListContentState.Skeleton -> {
- SkeletonView(
- count = contentState.count,
- )
- }
- is RoomListContentState.Empty -> {
- EmptyView(
- state = contentState,
- eventSink = eventSink,
- onSetUpRecoveryClick = onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onCreateRoomClick = onCreateRoomClick,
- )
- }
- is RoomListContentState.Rooms -> {
- RoomsView(
- state = contentState,
- hideInvitesAvatars = hideInvitesAvatars,
- filtersState = filtersState,
- eventSink = eventSink,
- onSetUpRecoveryClick = onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onRoomClick = onRoomClick,
- contentBottomPadding = contentBottomPadding,
- )
- }
+ when (contentState) {
+ is RoomListContentState.Skeleton -> {
+ SkeletonView(
+ modifier = modifier,
+ count = contentState.count,
+ contentPadding = contentPadding,
+ )
+ }
+ is RoomListContentState.Empty -> {
+ EmptyView(
+ modifier = modifier.padding(contentPadding),
+ state = contentState,
+ eventSink = eventSink,
+ onSetUpRecoveryClick = onSetUpRecoveryClick,
+ onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+ onCreateRoomClick = onCreateRoomClick,
+ )
+ }
+ is RoomListContentState.Rooms -> {
+ RoomsView(
+ modifier = modifier,
+ state = contentState,
+ hideInvitesAvatars = hideInvitesAvatars,
+ filtersState = filtersState,
+ eventSink = eventSink,
+ onSetUpRecoveryClick = onSetUpRecoveryClick,
+ onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+ onRoomClick = onRoomClick,
+ lazyListState = lazyListState,
+ contentPadding = contentPadding,
+ )
}
}
}
@Composable
-private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
- LazyColumn(modifier = modifier) {
+private fun SkeletonView(
+ count: Int,
+ contentPadding: PaddingValues,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier,
+ contentPadding = contentPadding,
+ ) {
repeat(count) { index ->
item {
RoomSummaryPlaceholderRow()
@@ -167,7 +179,8 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
+ lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@@ -183,7 +196,8 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
- contentBottomPadding = contentBottomPadding,
+ contentPadding = contentPadding,
+ lazyListState = lazyListState,
modifier = modifier.fillMaxSize(),
)
}
@@ -197,10 +211,10 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
+ lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
- val lazyListState = rememberLazyListState()
val visibleRange by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
@@ -216,7 +230,7 @@ private fun RoomsViewList(
LazyColumn(
state = lazyListState,
modifier = modifier,
- contentPadding = PaddingValues(bottom = contentBottomPadding)
+ contentPadding = contentPadding,
) {
when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> {
@@ -243,6 +257,12 @@ private fun RoomsViewList(
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
+ } else if (state.showNewNotificationSoundBanner) {
+ item {
+ NewNotificationSoundBanner(
+ onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
+ )
+ }
}
}
@@ -329,6 +349,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {},
onRoomClick = {},
onCreateRoomClick = {},
- contentBottomPadding = 0.dp,
+ lazyListState = rememberLazyListState(),
+ contentPadding = PaddingValues(0.dp),
)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt
index 9f17fc835e7..2cfc589131b 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt
deleted file mode 100644
index 8071dead3dd..00000000000
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt
+++ /dev/null
@@ -1,361 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.home.impl.components
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBars
-import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.material3.rememberTopAppBarState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.heading
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import io.element.android.appconfig.RoomListConfig
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.home.impl.R
-import io.element.android.features.home.impl.filters.RoomListFiltersState
-import io.element.android.features.home.impl.filters.RoomListFiltersView
-import io.element.android.features.home.impl.filters.aRoomListFiltersState
-import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
-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.AvatarSize
-import io.element.android.libraries.designsystem.components.avatar.AvatarType
-import io.element.android.libraries.designsystem.components.avatarBloom
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.text.applyScaleDown
-import io.element.android.libraries.designsystem.text.roundToPx
-import io.element.android.libraries.designsystem.text.toDp
-import io.element.android.libraries.designsystem.text.toSp
-import io.element.android.libraries.designsystem.theme.aliasScreenTitle
-import io.element.android.libraries.designsystem.theme.components.DropdownMenu
-import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
-import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
-import io.element.android.libraries.designsystem.theme.components.Icon
-import io.element.android.libraries.designsystem.theme.components.IconButton
-import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.matrix.api.core.UserId
-import io.element.android.libraries.matrix.api.user.MatrixUser
-import io.element.android.libraries.matrix.ui.model.getAvatarData
-import io.element.android.libraries.testtags.TestTags
-import io.element.android.libraries.testtags.testTag
-import io.element.android.libraries.ui.strings.CommonStrings
-
-private val avatarBloomSize = 430.dp
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun RoomListTopBar(
- title: String,
- matrixUser: MatrixUser,
- showAvatarIndicator: Boolean,
- areSearchResultsDisplayed: Boolean,
- onToggleSearch: () -> Unit,
- onMenuActionClick: (RoomListMenuAction) -> Unit,
- onOpenSettings: () -> Unit,
- scrollBehavior: TopAppBarScrollBehavior,
- displayMenuItems: Boolean,
- displayFilters: Boolean,
- filtersState: RoomListFiltersState,
- canReportBug: Boolean,
- modifier: Modifier = Modifier,
-) {
- DefaultRoomListTopBar(
- title = title,
- matrixUser = matrixUser,
- showAvatarIndicator = showAvatarIndicator,
- areSearchResultsDisplayed = areSearchResultsDisplayed,
- onOpenSettings = onOpenSettings,
- onSearchClick = onToggleSearch,
- onMenuActionClick = onMenuActionClick,
- scrollBehavior = scrollBehavior,
- displayMenuItems = displayMenuItems,
- displayFilters = displayFilters,
- filtersState = filtersState,
- canReportBug = canReportBug,
- modifier = modifier,
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun DefaultRoomListTopBar(
- title: String,
- matrixUser: MatrixUser,
- showAvatarIndicator: Boolean,
- areSearchResultsDisplayed: Boolean,
- scrollBehavior: TopAppBarScrollBehavior,
- onOpenSettings: () -> Unit,
- onSearchClick: () -> Unit,
- onMenuActionClick: (RoomListMenuAction) -> Unit,
- displayMenuItems: Boolean,
- displayFilters: Boolean,
- filtersState: RoomListFiltersState,
- canReportBug: Boolean,
- modifier: Modifier = Modifier,
-) {
- // We need this to manually clip the top app bar in preview mode
- val previewAppBarHeight = if (LocalInspectionMode.current) {
- 112.dp.roundToPx()
- } else {
- null
- }
- val collapsedFraction = scrollBehavior.state.collapsedFraction
- var appBarHeight by remember {
- mutableIntStateOf(previewAppBarHeight ?: 0)
- }
-
- val avatarData by remember(matrixUser) {
- derivedStateOf {
- matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
- }
- }
-
- val statusBarPadding = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
-
- Box(modifier = modifier) {
- val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle
- val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy(
- // Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
- // ensure that the font size will never be bigger than 28.dp.
- fontSize = 28.dp.applyScaleDown().toSp()
- )
- MaterialTheme(
- colorScheme = ElementTheme.materialColors,
- shapes = MaterialTheme.shapes,
- typography = ElementTheme.materialTypography.copy(
- headlineSmall = expandedTitleTextStyle,
- titleLarge = collapsedTitleTextStyle
- ),
- ) {
- Column(
- modifier = Modifier
- .onSizeChanged {
- appBarHeight = it.height
- }
- .avatarBloom(
- avatarData = avatarData,
- background = if (ElementTheme.isLightTheme) {
- // Workaround to display a very subtle bloom for avatars with very soft colors
- Color(0xFFF9F9F9)
- } else {
- ElementTheme.colors.bgCanvasDefault
- },
- blurSize = DpSize(avatarBloomSize, avatarBloomSize),
- offset = DpOffset(24.dp, 24.dp + statusBarPadding),
- clipToSize = if (appBarHeight > 0) {
- DpSize(
- avatarBloomSize,
- appBarHeight.toDp()
- )
- } else {
- DpSize.Unspecified
- },
- bottomSoftEdgeColor = ElementTheme.colors.bgCanvasDefault,
- bottomSoftEdgeAlpha = if (displayFilters) {
- 1f
- } else {
- 1f - collapsedFraction
- },
- alpha = if (areSearchResultsDisplayed) 0f else 1f,
- )
- .statusBarsPadding(),
- ) {
- MediumTopAppBar(
- colors = TopAppBarDefaults.mediumTopAppBarColors(
- containerColor = Color.Transparent,
- scrolledContainerColor = Color.Transparent,
- ),
- title = {
- Text(
- modifier = Modifier.semantics {
- heading()
- },
- text = title,
- )
- },
- navigationIcon = {
- NavigationIcon(
- avatarData = avatarData,
- showAvatarIndicator = showAvatarIndicator,
- onClick = onOpenSettings,
- )
- },
- actions = {
- if (displayMenuItems) {
- IconButton(
- onClick = onSearchClick,
- ) {
- Icon(
- imageVector = CompoundIcons.Search(),
- contentDescription = stringResource(CommonStrings.action_search),
- )
- }
- if (RoomListConfig.HAS_DROP_DOWN_MENU) {
- var showMenu by remember { mutableStateOf(false) }
- IconButton(
- onClick = { showMenu = !showMenu }
- ) {
- Icon(
- imageVector = CompoundIcons.OverflowVertical(),
- contentDescription = null,
- )
- }
- DropdownMenu(
- expanded = showMenu,
- onDismissRequest = { showMenu = false }
- ) {
- if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
- DropdownMenuItem(
- onClick = {
- showMenu = false
- onMenuActionClick(RoomListMenuAction.InviteFriends)
- },
- text = { Text(stringResource(id = CommonStrings.action_invite)) },
- leadingIcon = {
- Icon(
- imageVector = CompoundIcons.ShareAndroid(),
- tint = ElementTheme.colors.iconSecondary,
- contentDescription = null,
- )
- }
- )
- }
- if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
- DropdownMenuItem(
- onClick = {
- showMenu = false
- onMenuActionClick(RoomListMenuAction.ReportBug)
- },
- text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
- leadingIcon = {
- Icon(
- imageVector = CompoundIcons.ChatProblem(),
- tint = ElementTheme.colors.iconSecondary,
- contentDescription = null,
- )
- }
- )
- }
- }
- }
- }
- },
- scrollBehavior = scrollBehavior,
- windowInsets = WindowInsets(0.dp),
- )
- if (displayFilters) {
- RoomListFiltersView(
- state = filtersState,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- }
- }
- }
-
- HorizontalDivider(
- modifier = Modifier
- .fillMaxWidth()
- .alpha(collapsedFraction)
- .align(Alignment.BottomCenter),
- color = ElementTheme.materialColors.outlineVariant,
- )
- }
-}
-
-@Composable
-private fun NavigationIcon(
- avatarData: AvatarData,
- showAvatarIndicator: Boolean,
- onClick: () -> Unit,
-) {
- IconButton(
- modifier = Modifier.testTag(TestTags.homeScreenSettings),
- onClick = onClick,
- ) {
- Box {
- Avatar(
- avatarData = avatarData,
- avatarType = AvatarType.User,
- contentDescription = stringResource(CommonStrings.common_settings),
- )
- if (showAvatarIndicator) {
- RedIndicatorAtom(
- modifier = Modifier.align(Alignment.TopEnd)
- )
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@PreviewsDayNight
-@Composable
-internal fun DefaultRoomListTopBarPreview() = ElementPreview {
- DefaultRoomListTopBar(
- title = stringResource(R.string.screen_roomlist_main_space_title),
- matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
- showAvatarIndicator = false,
- areSearchResultsDisplayed = false,
- scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
- onOpenSettings = {},
- onSearchClick = {},
- displayMenuItems = true,
- displayFilters = true,
- filtersState = aRoomListFiltersState(),
- canReportBug = true,
- onMenuActionClick = {},
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@PreviewsDayNight
-@Composable
-internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
- DefaultRoomListTopBar(
- title = stringResource(R.string.screen_roomlist_main_space_title),
- matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
- showAvatarIndicator = true,
- areSearchResultsDisplayed = false,
- scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
- onOpenSettings = {},
- onSearchClick = {},
- displayMenuItems = true,
- displayFilters = true,
- filtersState = aRoomListFiltersState(),
- canReportBug = true,
- onMenuActionClick = {},
- )
-}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt
index f07fe880b35..466abb76d3e 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
index e5a85a7442d..a9a1745b853 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -35,6 +36,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
@@ -44,14 +46,13 @@ import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
+import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
+import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
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.Icon
-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.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
@@ -99,7 +100,7 @@ internal fun RoomSummaryRow(
)
}
Spacer(modifier = Modifier.height(12.dp))
- InviteButtonsRow(
+ InviteButtonsRowMolecule(
onAcceptClick = {
eventSink(RoomListEvents.AcceptInvite(room))
},
@@ -170,14 +171,15 @@ private fun RoomSummaryScaffoldRow(
hideAvatarImage: Boolean = false,
content: @Composable ColumnScope.() -> Unit
) {
- val clickModifier = Modifier.combinedClickable(
- onClick = { onClick(room) },
- onLongClick = { onLongClick(room) },
- onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
- indication = ripple(),
- interactionSource = remember { MutableInteractionSource() }
- )
-
+ val clickModifier = Modifier
+ .combinedClickable(
+ onClick = { onClick(room) },
+ onLongClick = { onLongClick(room) },
+ onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
+ indication = ripple(),
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ .onKeyboardContextMenuAction { onLongClick(room) }
Row(
modifier = modifier
.fillMaxWidth()
@@ -188,10 +190,14 @@ private fun RoomSummaryScaffoldRow(
) {
Avatar(
avatarData = room.avatarData,
- avatarType = AvatarType.Room(
- heroes = room.heroes,
- isTombstoned = room.isTombstoned,
- ),
+ avatarType = if (room.isSpace) {
+ AvatarType.Space(isTombstoned = room.isTombstoned)
+ } else {
+ AvatarType.Room(
+ heroes = room.heroes,
+ isTombstoned = room.isTombstoned,
+ )
+ },
hideImage = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
@@ -219,7 +225,7 @@ private fun NameAndTimestampRow(
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
- color = ElementTheme.roomListRoomName(),
+ color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -230,7 +236,7 @@ private fun NameAndTimestampRow(
color = if (isHighlighted) {
ElementTheme.colors.unreadIndicator
} else {
- ElementTheme.roomListRoomMessageDate()
+ ElementTheme.colors.roomListRoomMessageDate
},
)
}
@@ -253,7 +259,7 @@ private fun InviteSubtitle(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular,
- color = ElementTheme.roomListRoomMessage(),
+ color = ElementTheme.colors.roomListRoomMessage,
modifier = modifier,
)
}
@@ -277,15 +283,19 @@ private fun MessagePreviewAndIndicatorRow(
Text(
modifier = Modifier.weight(1f),
text = annotatedMessagePreview,
- color = ElementTheme.roomListRoomMessage(),
+ color = ElementTheme.colors.roomListRoomMessage,
style = ElementTheme.typography.fontBodyMdRegular,
minLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
+
// Call and unread
Row(
- modifier = Modifier.height(16.dp),
+ modifier = Modifier
+ .height(16.dp)
+ // Used to force this line to be read aloud earlier than the latest event when using Talkback
+ .zIndex(-1f),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -301,8 +311,10 @@ private fun MessagePreviewAndIndicatorRow(
MentionIndicatorAtom()
}
if (room.hasNewContent) {
+ val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
UnreadIndicatorAtom(
- color = tint
+ color = tint,
+ contentDescription = contentDescription,
)
}
}
@@ -325,7 +337,7 @@ private fun InviteNameAndIndicatorRow(
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
- color = ElementTheme.roomListRoomName(),
+ color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -337,31 +349,6 @@ private fun InviteNameAndIndicatorRow(
}
}
-@Composable
-private fun InviteButtonsRow(
- onAcceptClick: () -> Unit,
- onDeclineClick: () -> Unit,
- modifier: Modifier = Modifier
-) {
- Row(
- modifier = modifier,
- horizontalArrangement = spacedBy(12.dp)
- ) {
- OutlinedButton(
- text = stringResource(CommonStrings.action_decline),
- onClick = onDeclineClick,
- size = ButtonSize.MediumLowPadding,
- modifier = Modifier.weight(1f),
- )
- Button(
- text = stringResource(CommonStrings.action_accept),
- onClick = onAcceptClick,
- size = ButtonSize.MediumLowPadding,
- modifier = Modifier.weight(1f),
- )
- }
-}
-
@Composable
private fun OnGoingCallIcon(
color: Color,
@@ -369,7 +356,7 @@ private fun OnGoingCallIcon(
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid(),
- contentDescription = null,
+ contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call),
tint = color,
)
}
@@ -378,7 +365,7 @@ private fun OnGoingCallIcon(
private fun NotificationOffIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
- contentDescription = null,
+ contentDescription = stringResource(CommonStrings.a11y_notifications_muted),
imageVector = CompoundIcons.NotificationsOffSolid(),
tint = ElementTheme.colors.iconQuaternary,
)
@@ -388,7 +375,7 @@ private fun NotificationOffIndicatorAtom() {
private fun MentionIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
- contentDescription = null,
+ contentDescription = stringResource(CommonStrings.a11y_notifications_new_mentions),
imageVector = CompoundIcons.Mention(),
tint = ElementTheme.colors.unreadIndicator,
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt
index 37aed6d1302..0d3bf87b07a 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
index dcda64ca9cc..ef5d1ffcc63 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
@@ -1,12 +1,14 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.datasource
+import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
@@ -29,10 +31,11 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-import javax.inject.Inject
+import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
-class RoomListDataSource @Inject constructor(
+@Inject
+class RoomListDataSource(
private val roomListService: RoomListService,
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
private val coroutineDispatchers: CoroutineDispatchers,
@@ -101,13 +104,44 @@ class RoomListDataSource @Inject constructor(
}
private suspend fun buildAndEmitAllRooms(roomSummaries: List, useCache: Boolean = true) {
+ // Used to detect duplicates in the room list summaries - see comment below
+ data class CacheResult(val index: Int, val fromCache: Boolean)
+ val cachingResults = mutableMapOf>()
+
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
if (useCache) {
- diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index)
+ diffCache.get(index)?.let { cachedItem ->
+ // Add the cached item to the caching results
+ val pairs = cachingResults.getOrDefault(cachedItem.roomId, mutableListOf())
+ pairs.add(CacheResult(index, fromCache = true))
+ cachingResults[cachedItem.roomId] = pairs
+ cachedItem
+ } ?: run {
+ roomSummaries.getOrNull(index)?.roomId?.let {
+ // Add the non-cached item to the caching results
+ val pairs = cachingResults.getOrDefault(it, mutableListOf())
+ pairs.add(CacheResult(index, fromCache = false))
+ cachingResults[it] = pairs
+ }
+ buildAndCacheItem(roomSummaries, index)
+ }
} else {
+ roomSummaries.getOrNull(index)?.roomId?.let {
+ // Add the non-cached item to the caching results
+ val pairs = cachingResults.getOrDefault(it, mutableListOf())
+ pairs.add(CacheResult(index, fromCache = false))
+ cachingResults[it] = pairs
+ }
buildAndCacheItem(roomSummaries, index)
}
}
+
+ // TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
+ val duplicates = cachingResults.filter { (_, operations) -> operations.size > 1 }
+ if (duplicates.isNotEmpty()) {
+ Timber.e("Found duplicates in room summaries after an UI update: $duplicates. This could be a race condition/caching issue of some kind")
+ }
+
_allRooms.emit(roomListRoomSummaries.toImmutableList())
}
@@ -119,7 +153,7 @@ class RoomListDataSource @Inject constructor(
private suspend fun rebuildAllRoomSummaries() {
lock.withLock {
- roomListService.allRooms.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
+ roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
buildAndEmitAllRooms(roomSummaries, useCache = false)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt
index 3d776621887..62b34ba8dec 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt
@@ -1,12 +1,14 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.datasource
+import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty
@@ -20,9 +22,9 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.collections.immutable.toImmutableList
-import javax.inject.Inject
-class RoomListRoomSummaryFactory @Inject constructor(
+@Inject
+class RoomListRoomSummaryFactory(
private val dateFormatter: DateFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
) {
@@ -68,6 +70,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
user.getAvatarData(size = AvatarSize.RoomListItem)
}.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null,
+ isSpace = roomInfo.isSpace,
)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt
new file mode 100644
index 00000000000..5e2212375d5
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.di
+
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
+import io.element.android.features.home.impl.spaces.HomeSpacesPresenter
+import io.element.android.features.home.impl.spaces.HomeSpacesState
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.SessionScope
+
+@BindingContainer
+@ContributesTo(SessionScope::class)
+interface HomeSpacesModule {
+ @Binds
+ fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt
index ef645124622..ea80c1b3cbc 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt
@@ -1,15 +1,16 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.home.impl.filters.RoomListFiltersPresenter
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.roomlist.RoomListPresenter
@@ -20,7 +21,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
-@Module
+@BindingContainer
interface RoomListModule {
@Binds
fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt
index 2c7ebc35736..1f627eca4ef 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt
index 61436a0054e..7381ac308ed 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt
index 13fcc656e81..8b1906d28f6 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt
index de3f7eaa0bc..4b20c69751f 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,23 +11,24 @@ package io.element.android.features.home.impl.filters
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
+import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
-import javax.inject.Inject
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
-class RoomListFiltersPresenter @Inject constructor(
+@Inject
+class RoomListFiltersPresenter(
private val roomListService: RoomListService,
private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter {
- private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toPersistentList()
+ private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
@Composable
override fun present(): RoomListFiltersState {
- fun handleEvents(event: RoomListFiltersEvents) {
+ fun handleEvent(event: RoomListFiltersEvents) {
when (event) {
RoomListFiltersEvents.ClearSelectedFilters -> {
filterSelectionStrategy.clear()
@@ -40,7 +42,7 @@ class RoomListFiltersPresenter @Inject constructor(
val filters by produceState(initialValue = initialFilters) {
filterSelectionStrategy.filterSelectionStates
.map { filters ->
- value = filters.toPersistentList()
+ value = filters.toImmutableList()
filters.mapNotNull { filterState ->
if (!filterState.isSelected) {
return@mapNotNull null
@@ -62,7 +64,7 @@ class RoomListFiltersPresenter @Inject constructor(
return RoomListFiltersState(
filterSelectionStates = filters,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
index bb4146ce84b..104a99cfbd3 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,7 +10,7 @@ package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class RoomListFiltersState(
val filterSelectionStates: ImmutableList,
@@ -21,6 +22,6 @@ data class RoomListFiltersState(
return filterSelectionStates
.filter { it.isSelected }
.map { it.filter }
- .toPersistentList()
+ .toImmutableList()
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt
index 8cba01163c1..7d737274847 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
index 91b00b1180d..588da6b9db9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -19,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
@@ -34,6 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -48,6 +51,9 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
+/**
+ * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2191-606
+ */
@Composable
fun RoomListFiltersView(
state: RoomListFiltersState,
@@ -143,9 +149,12 @@ private fun RoomListClearFiltersButton(
.clip(CircleShape)
.background(ElementTheme.colors.bgActionPrimaryRest)
.clickable(onClick = onClick)
+ .padding(4.dp)
) {
Icon(
- modifier = Modifier.align(Alignment.Center),
+ modifier = Modifier
+ .align(Alignment.Center)
+ .size(16.dp),
imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters),
@@ -170,21 +179,34 @@ private fun RoomListFilterView(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip text colour",
)
+ val borderColour = animateColorAsState(
+ targetValue = if (selected) Color.Transparent else ElementTheme.colors.borderInteractiveSecondary,
+ animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
+ label = "chip border colour",
+ )
FilterChip(
selected = selected,
onClick = { onClick(roomListFilter) },
- modifier = modifier.height(36.dp),
+ modifier = modifier.height(32.dp),
shape = CircleShape,
colors = FilterChipDefaults.filterChipColors(
containerColor = background.value,
selectedContainerColor = background.value,
labelColor = textColour.value,
- selectedLabelColor = textColour.value
+ selectedLabelColor = textColour.value,
),
label = {
- Text(text = stringResource(id = roomListFilter.stringResource))
- }
+ Text(
+ text = stringResource(id = roomListFilter.stringResource),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ )
+ },
+ border = FilterChipDefaults.filterChipBorder(
+ enabled = true,
+ selected = selected,
+ borderColor = borderColour.value,
+ ),
)
}
@@ -192,6 +214,7 @@ private fun RoomListFilterView(
@Composable
internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView(
+ modifier = Modifier.padding(vertical = 4.dp),
state = state,
)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt
index 26a77da5c78..877e9347279 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt
@@ -1,20 +1,20 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters.selection
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.home.impl.filters.RoomListFilter
import io.element.android.libraries.di.SessionScope
import kotlinx.coroutines.flow.MutableStateFlow
-import javax.inject.Inject
@ContributesBinding(SessionScope::class)
-class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
+class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
private val selectedFilters = LinkedHashSet()
override val filterSelectionStates = MutableStateFlow(buildFilters())
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt
index d604e51f840..3a136885823 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt
index 8396a4e2a22..f0877b5e0d9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
index 3f166a66b44..215ded20813 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -38,6 +39,7 @@ data class RoomListRoomSummary(
val inviteSender: InviteSender?,
val isTombstoned: Boolean,
val heroes: ImmutableList,
+ val isSpace: Boolean,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
index f06e5a1a278..e00d7564ba9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -102,6 +103,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider = emptyList(),
isTombstoned: Boolean = false,
+ isSpace: Boolean = false,
) = RoomListRoomSummary(
id = id,
roomId = RoomId(id),
@@ -172,4 +183,5 @@ internal fun aRoomListRoomSummary(
canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
+ isSpace = isSpace
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt
index 8fcef46faf3..a27a83d9552 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
index a421c239cb4..c79e79be736 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,7 +17,7 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentSet
+import kotlinx.collections.immutable.toImmutableSet
open class RoomListContentStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -26,21 +27,26 @@ open class RoomListContentStateProvider : PreviewParameterProvider = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
+ showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,
- seenRoomInvites = seenRoomInvites.toPersistentSet(),
+ seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
index 27883e88d59..5c7ef158fd7 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -60,7 +61,7 @@ fun RoomListContextMenu(
},
onLeaveRoomClick = {
eventSink(RoomListEvents.HideContextMenu)
- eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
+ eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
},
onFavoriteChange = { isFavorite ->
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
@@ -101,36 +102,34 @@ private fun RoomListModalBottomSheetContent(
)
}
)
- if (contextMenu.markAsUnreadFeatureFlagEnabled) {
- if (contextMenu.hasNewContent) {
- ListItem(
- headlineContent = {
- Text(
- text = stringResource(id = R.string.screen_roomlist_mark_as_read),
- style = MaterialTheme.typography.bodyLarge,
- )
- },
- onClick = onRoomMarkReadClick,
- leadingContent = ListItemContent.Icon(
- iconSource = IconSource.Vector(CompoundIcons.MarkAsRead())
- ),
- style = ListItemStyle.Primary,
- )
- } else {
- ListItem(
- headlineContent = {
- Text(
- text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
- style = MaterialTheme.typography.bodyLarge,
- )
- },
- onClick = onRoomMarkUnreadClick,
- leadingContent = ListItemContent.Icon(
- iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread())
- ),
- style = ListItemStyle.Primary,
- )
- }
+ if (contextMenu.hasNewContent) {
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_roomlist_mark_as_read),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ onClick = onRoomMarkReadClick,
+ leadingContent = ListItemContent.Icon(
+ iconSource = IconSource.Vector(CompoundIcons.MarkAsRead())
+ ),
+ style = ListItemStyle.Primary,
+ )
+ } else {
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ onClick = onRoomMarkUnreadClick,
+ leadingContent = ListItemContent.Icon(
+ iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread())
+ ),
+ style = ListItemStyle.Primary,
+ )
}
ListItem(
headlineContent = {
@@ -176,7 +175,6 @@ private fun RoomListModalBottomSheetContent(
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.ChatProblem(),
- contentDescription = stringResource(CommonStrings.action_report_room),
)
),
style = ListItemStyle.Destructive,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
index c6b91a02fce..4c91e14dcd9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
index 4ba37d810b8..36953224c32 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,6 +15,7 @@ sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissBanner : RoomListEvents
+ data object DismissNewNotificationSoundBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
@@ -24,7 +26,7 @@ sealed interface RoomListEvents {
sealed interface ContextMenuEvents : RoomListEvents
data object HideContextMenu : ContextMenuEvents
- data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents
+ data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvents
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
index 45e4dd4238e..6493d76d981 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,7 +23,10 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
+import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.Interaction
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.search.RoomListSearchEvents
@@ -31,28 +35,25 @@ import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
-import io.element.android.features.leaveroom.api.LeaveRoomEvent.ShowConfirmation
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.core.coroutine.mapState
-import io.element.android.libraries.featureflag.api.FeatureFlagService
-import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
-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.RoomList
import io.element.android.libraries.matrix.api.timeline.ReceiptType
+import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
-import kotlinx.collections.immutable.toPersistentList
-import kotlinx.collections.immutable.toPersistentSet
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -65,16 +66,15 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
-import javax.inject.Inject
private const val EXTENDED_RANGE_SIZE = 40
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
-class RoomListPresenter @Inject constructor(
+@Inject
+class RoomListPresenter(
private val client: MatrixClient,
private val leaveRoomPresenter: Presenter,
private val roomListDataSource: RoomListDataSource,
- private val featureFlagService: FeatureFlagService,
private val filtersPresenter: Presenter,
private val searchPresenter: Presenter,
private val sessionPreferencesStore: SessionPreferencesStore,
@@ -85,8 +85,9 @@ class RoomListPresenter @Inject constructor(
private val notificationCleaner: NotificationCleaner,
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
+ private val announcementService: AnnouncementService,
) : Presenter {
- private val encryptionService: EncryptionService = client.encryptionService()
+ private val encryptionService = client.encryptionService
@Composable
override fun present(): RoomListState {
@@ -101,25 +102,28 @@ class RoomListPresenter @Inject constructor(
}
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
+ val showNewNotificationSoundBanner by remember {
+ announcementService.announcementsToShowFlow().map { announcements ->
+ announcements.contains(Announcement.NewNotificationSound)
+ }
+ }.collectAsState(false)
// Avatar indicator
- val hideInvitesAvatar by remember {
- client
- .mediaPreviewService()
- .mediaPreviewConfigFlow
- .mapState { config -> config.hideInviteAvatar }
- }.collectAsState()
+ val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) }
val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) }
- fun handleEvents(event: RoomListEvents) {
+ fun handleEvent(event: RoomListEvents) {
when (event) {
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
updateVisibleRange(event.range)
}
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissBanner -> securityBannerDismissed = true
+ RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
+ announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
+ }
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu)
@@ -127,7 +131,9 @@ class RoomListPresenter @Inject constructor(
is RoomListEvents.HideContextMenu -> {
contextMenu.value = RoomListState.ContextMenu.Hidden
}
- is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(ShowConfirmation(event.roomId))
+ is RoomListEvents.LeaveRoom -> {
+ leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
+ }
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
@@ -147,7 +153,10 @@ class RoomListPresenter @Inject constructor(
}
}
- val contentState = roomListContentState(securityBannerDismissed)
+ val contentState = roomListContentState(
+ securityBannerDismissed,
+ showNewNotificationSoundBanner,
+ )
val canReportRoom by produceState(false) { value = client.canReportRoom() }
@@ -161,7 +170,7 @@ class RoomListPresenter @Inject constructor(
acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatar,
canReportRoom = canReportRoom,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
@@ -203,6 +212,7 @@ class RoomListPresenter @Inject constructor(
@Composable
private fun roomListContentState(
securityBannerDismissed: Boolean,
+ showNewNotificationSoundBanner: Boolean,
): RoomListContentState {
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
@@ -221,15 +231,18 @@ class RoomListPresenter @Inject constructor(
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
return when {
- showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState)
+ showEmpty -> RoomListContentState.Empty(
+ securityBannerState = securityBannerState,
+ )
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
+ showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
batteryOptimizationState = batteryOptimizationPresenter.present(),
- summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
- seenRoomInvites = seenRoomInvites.toPersistentSet(),
+ summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
+ seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
}
}
@@ -242,7 +255,6 @@ class RoomListPresenter @Inject constructor(
roomName = event.roomSummary.name,
isDm = event.roomSummary.isDm,
isFavorite = event.roomSummary.isFavorite,
- markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
hasNewContent = event.roomSummary.hasNewContent,
displayClearRoomCacheAction = appPreferencesStore.isDeveloperModeEnabledFlow().first(),
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
index 4cfa82fbfcd..2b5ca0391e9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -19,7 +20,6 @@ import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
-@Immutable
data class RoomListState(
val contextMenu: ContextMenu,
val declineInviteMenu: DeclineInviteMenu,
@@ -41,7 +41,6 @@ data class RoomListState(
val roomName: String?,
val isDm: Boolean,
val isFavorite: Boolean,
- val markAsUnreadFeatureFlagEnabled: Boolean,
val hasNewContent: Boolean,
val displayClearRoomCacheAction: Boolean,
) : ContextMenu
@@ -70,6 +69,7 @@ sealed interface RoomListContentState {
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
+ val showNewNotificationSoundBanner: Boolean,
val summaries: ImmutableList,
val seenRoomInvites: ImmutableSet,
) : RoomListContentState
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt
index 0cd3117de4c..3f2499657e3 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -28,7 +29,6 @@ internal fun aContextMenuShown(
roomId = RoomId("!aRoom:aDomain"),
roomName = roomName,
isDm = isDm,
- markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
displayClearRoomCacheAction = false,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
index 0e4600c16ad..bf47d667f1e 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,18 +17,16 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary
import io.element.android.features.home.impl.model.anInviteSender
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
+import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
-import io.element.android.features.leaveroom.api.aLeaveRoomState
-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.RoomId
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
open class RoomListStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -42,6 +41,7 @@ open class RoomListStateProvider : PreviewParameterProvider {
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
+ aRoomListState(contentState = anEmptyContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
)
}
@@ -69,15 +69,11 @@ internal fun aRoomListState(
eventSink = eventSink,
)
-internal fun anAcceptDeclineInviteState(
- acceptAction: AsyncAction = AsyncAction.Uninitialized,
- declineAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
-) = AcceptDeclineInviteState(
- acceptAction = acceptAction,
- declineAction = declineAction,
- eventSink = eventSink,
-)
+internal fun aLeaveRoomState(
+ eventSink: (LeaveRoomEvent) -> Unit = {}
+) = object : LeaveRoomState {
+ override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
+}
internal fun aRoomListRoomSummaryList(): ImmutableList {
return persistentListOf(
@@ -127,5 +123,5 @@ internal fun generateRoomListRoomSummaryList(
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
id = "!roomId$index:domain",
)
- }.toPersistentList()
+ }.toImmutableList()
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
index 6840809480c..44f53a26632 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
@@ -1,12 +1,14 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
+import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -14,17 +16,17 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
-import kotlinx.collections.immutable.PersistentList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import javax.inject.Inject
private const val PAGE_SIZE = 30
-class RoomListSearchDataSource @Inject constructor(
+@Inject
+class RoomListSearchDataSource(
roomListService: RoomListService,
coroutineDispatchers: CoroutineDispatchers,
private val roomSummaryFactory: RoomListRoomSummaryFactory,
@@ -35,11 +37,11 @@ class RoomListSearchDataSource @Inject constructor(
source = RoomList.Source.All,
)
- val roomSummaries: Flow> = roomList.filteredSummaries
+ val roomSummaries: Flow> = roomList.filteredSummaries
.map { roomSummaries ->
roomSummaries
.map(roomSummaryFactory::create)
- .toPersistentList()
+ .toImmutableList()
}
.flowOn(coroutineDispatchers.computation)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt
index 26f713da819..20222c144dd 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt
index b4d151fe028..ad06b12f5e3 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,11 +15,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.persistentListOf
-import javax.inject.Inject
-class RoomListSearchPresenter @Inject constructor(
+@Inject
+class RoomListSearchPresenter(
private val dataSource: RoomListSearchDataSource,
) : Presenter {
@Composable
@@ -39,7 +41,7 @@ class RoomListSearchPresenter @Inject constructor(
dataSource.setSearchQuery(searchQuery)
}
- fun handleEvents(event: RoomListSearchEvents) {
+ fun handleEvent(event: RoomListSearchEvents) {
when (event) {
RoomListSearchEvents.ClearQuery -> {
searchQuery = ""
@@ -60,7 +62,7 @@ class RoomListSearchPresenter @Inject constructor(
isSearchActive = isSearchActive,
query = searchQuery,
results = searchResults,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt
index 9d50f52a339..92e8c1ff034 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt
index 503e334418e..a5015a5003a 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
index 29c8078c09b..58d6ba7e003 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -20,17 +21,21 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -39,7 +44,6 @@ import io.element.android.features.home.impl.contentType
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.designsystem.components.button.BackButton
-import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FilledTextField
@@ -47,7 +51,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
-import io.element.android.libraries.designsystem.utils.copy
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@@ -68,24 +71,13 @@ internal fun RoomListSearchView(
enter = fadeIn(),
exit = fadeOut(),
) {
- Column(
- modifier = modifier
- .applyIf(
- condition = state.isSearchActive,
- ifTrue = {
- // Disable input interaction to underlying views
- pointerInput(Unit) {}
- }
- )
- ) {
- if (state.isSearchActive) {
- RoomListSearchContent(
- state = state,
- hideInvitesAvatars = hideInvitesAvatars,
- onRoomClick = onRoomClick,
- eventSink = eventSink,
- )
- }
+ Column(modifier = modifier) {
+ RoomListSearchContent(
+ state = state,
+ hideInvitesAvatars = hideInvitesAvatars,
+ onRoomClick = onRoomClick,
+ eventSink = eventSink,
+ )
}
}
}
@@ -120,15 +112,23 @@ private fun RoomListSearchContent(
},
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
title = {
- val filter = state.query
- val focusRequester = FocusRequester()
+ // TODO replace `state.query` with TextFieldState when it's available for M3 TextField
+ // The stateSaver will keep the selection state when returning to this UI
+ var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(TextFieldValue(state.query))
+ }
+
+ val focusRequester = remember { FocusRequester() }
FilledTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
- value = filter,
+ value = value,
singleLine = true,
- onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) },
+ onValueChange = {
+ value = it
+ state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
+ },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
@@ -139,9 +139,11 @@ private fun RoomListSearchContent(
errorIndicatorColor = Color.Transparent,
),
trailingIcon = {
- if (filter.isNotEmpty()) {
+ if (value.text.isNotEmpty()) {
IconButton(onClick = {
state.eventSink(RoomListSearchEvents.ClearQuery)
+ // Clear local state too
+ value = value.copy(text = "")
}) {
Icon(
imageVector = CompoundIcons.Close(),
@@ -152,13 +154,13 @@ private fun RoomListSearchContent(
}
)
- LaunchedEffect(state.isSearchActive) {
- if (state.isSearchActive) {
+ LaunchedEffect(Unit) {
+ if (!focusRequester.restoreFocusedChild()) {
focusRequester.requestFocus()
}
+ focusRequester.saveFocusedChild()
}
},
- windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)
)
}
) { padding ->
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt
new file mode 100644
index 00000000000..88290417dd4
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+sealed interface HomeSpacesEvents
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt
new file mode 100644
index 00000000000..a890a61ac3c
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import dev.zacsweers.metro.Inject
+import io.element.android.features.invite.api.SeenInvitesStore
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentSetOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
+import kotlinx.coroutines.flow.map
+
+@Inject
+class HomeSpacesPresenter(
+ private val client: MatrixClient,
+ private val seenInvitesStore: SeenInvitesStore,
+) : Presenter {
+ @Composable
+ override fun present(): HomeSpacesState {
+ val hideInvitesAvatar by client.rememberHideInvitesAvatar()
+ val spaceRooms by remember {
+ client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
+ }.collectAsState(persistentListOf())
+
+ val seenSpaceInvites by remember {
+ seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
+ }.collectAsState(persistentSetOf())
+
+ fun handleEvent(event: HomeSpacesEvents) {
+ // when (event) { }
+ }
+
+ return HomeSpacesState(
+ space = CurrentSpace.Root,
+ spaceRooms = spaceRooms,
+ seenSpaceInvites = seenSpaceInvites,
+ hideInvitesAvatar = hideInvitesAvatar,
+ eventSink = ::handleEvent,
+ )
+ }
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt
new file mode 100644
index 00000000000..7dcb3702196
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableSet
+
+data class HomeSpacesState(
+ val space: CurrentSpace,
+ val spaceRooms: ImmutableList,
+ val seenSpaceInvites: ImmutableSet,
+ val hideInvitesAvatar: Boolean,
+ val eventSink: (HomeSpacesEvents) -> Unit,
+)
+
+sealed interface CurrentSpace {
+ object Root : CurrentSpace
+ data class Space(val spaceRoom: SpaceRoom) : CurrentSpace
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt
new file mode 100644
index 00000000000..8c03cff7ee4
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
+import io.element.android.libraries.previewutils.room.aSpaceRoom
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
+
+open class HomeSpacesStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aHomeSpacesState(
+ spaceRooms = SpaceRoomProvider().values.toList(),
+ seenSpaceInvites = setOf(
+ RoomId("!spaceId3:example.com"),
+ ),
+ ),
+ aHomeSpacesState(
+ space = CurrentSpace.Space(
+ spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
+ ),
+ spaceRooms = aListOfSpaceRooms(),
+ ),
+ )
+}
+
+internal fun aHomeSpacesState(
+ space: CurrentSpace = CurrentSpace.Root,
+ spaceRooms: List = aListOfSpaceRooms(),
+ seenSpaceInvites: Set = emptySet(),
+ hideInvitesAvatar: Boolean = false,
+ eventSink: (HomeSpacesEvents) -> Unit = {},
+) = HomeSpacesState(
+ space = space,
+ spaceRooms = spaceRooms.toImmutableList(),
+ seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
+ hideInvitesAvatar = hideInvitesAvatar,
+ eventSink = eventSink,
+)
+
+fun aListOfSpaceRooms(): List {
+ return listOf(
+ aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
+ aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
+ aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
+ )
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt
new file mode 100644
index 00000000000..2505cf831de
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.CurrentUserMembership
+import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
+import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
+import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+fun HomeSpacesView(
+ state: HomeSpacesState,
+ lazyListState: LazyListState,
+ onSpaceClick: (RoomId) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier,
+ state = lazyListState
+ ) {
+ val space = state.space
+ when (space) {
+ CurrentSpace.Root -> {
+ item {
+ SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
+ }
+ }
+ is CurrentSpace.Space -> item {
+ SpaceHeaderView(
+ avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
+ name = space.spaceRoom.displayName,
+ topic = space.spaceRoom.topic,
+ visibility = space.spaceRoom.visibility,
+ heroes = space.spaceRoom.heroes.toImmutableList(),
+ numberOfMembers = space.spaceRoom.numJoinedMembers,
+ )
+ }
+ }
+ item {
+ HorizontalDivider()
+ }
+ itemsIndexed(
+ items = state.spaceRooms,
+ key = { _, spaceRoom -> spaceRoom.roomId }
+ ) { index, spaceRoom ->
+ val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
+ SpaceRoomItemView(
+ spaceRoom = spaceRoom,
+ showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
+ hideAvatars = isInvitation && state.hideInvitesAvatar,
+ onClick = {
+ onSpaceClick(spaceRoom.roomId)
+ },
+ onLongClick = {
+ // TODO
+ },
+ )
+ if (index != state.spaceRooms.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun HomeSpacesViewPreview(
+ @PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState,
+) = ElementPreview {
+ HomeSpacesView(
+ state = state,
+ lazyListState = rememberLazyListState(),
+ onSpaceClick = {},
+ modifier = Modifier,
+ )
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt
new file mode 100644
index 00000000000..4a77e45372d
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.CurrentUserMembership
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
+import io.element.android.libraries.previewutils.room.aSpaceRoom
+
+class SpaceRoomProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(
+ aSpaceRoom(),
+ aSpaceRoom(
+ numJoinedMembers = 5,
+ childrenCount = 10,
+ worldReadable = true,
+ roomId = RoomId("!spaceId0:example.com"),
+ ),
+ aSpaceRoom(
+ numJoinedMembers = 5,
+ childrenCount = 10,
+ worldReadable = true,
+ avatarUrl = "anUrl",
+ roomId = RoomId("!spaceId1:example.com"),
+ ),
+ aSpaceRoom(
+ numJoinedMembers = 5,
+ childrenCount = 10,
+ worldReadable = true,
+ avatarUrl = "anUrl",
+ roomId = RoomId("!spaceId2:example.com"),
+ state = CurrentUserMembership.INVITED,
+ ),
+ )
+}
diff --git a/features/home/impl/src/main/res/values-be/translations.xml b/features/home/impl/src/main/res/values-be/translations.xml
index b34671ed861..d6cd4a1e717 100644
--- a/features/home/impl/src/main/res/values-be/translations.xml
+++ b/features/home/impl/src/main/res/values-be/translations.xml
@@ -7,6 +7,7 @@
"Ваша сховішча ключоў не сінхранізавана""Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны.""Палепшыце якасць званкоў"
+ "Усе чаты""Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?""Адхіліць запрашэнне""Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"
diff --git a/features/home/impl/src/main/res/values-bg/translations.xml b/features/home/impl/src/main/res/values-bg/translations.xml
index 8a555ebbb8d..44a0bd575ed 100644
--- a/features/home/impl/src/main/res/values-bg/translations.xml
+++ b/features/home/impl/src/main/res/values-bg/translations.xml
@@ -3,10 +3,16 @@
"Потвърдете ключа си за възстановяване, за да запазите достъп до хранилището за ключове и историята на съобщенията си.""Въведете ключа си за възстановяване""Хранилището ви за ключове не е синхронизирано"
+ "Всички чатове"
+ "Пространства""Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?""Отказване на покана"
+ "Сигурни ли сте, че искате да откажете този личен чат с %1$s?"
+ "Отказване на чат""Няма покани""%1$s (%2$s) ви покани"
+ "Това е еднократен процес, благодаря, че изчакахте."
+ "Настройване на вашия акаунт.""Създаване на нов разговор или стая""Започнете, като изпратите съобщение на някого.""Все още няма чатове."
diff --git a/features/home/impl/src/main/res/values-cs/translations.xml b/features/home/impl/src/main/res/values-cs/translations.xml
index 9fcea2887a1..1eab814e06b 100644
--- a/features/home/impl/src/main/res/values-cs/translations.xml
+++ b/features/home/impl/src/main/res/values-cs/translations.xml
@@ -3,6 +3,8 @@
"Zakažte optimalizaci baterie pro tuto aplikaci, abyste měli jistotu, že budou přijata všechna oznámení.""Zakázat optimalizaci""Nepřicházejí vám oznámení?"
+ "Váš zvuk oznámení byl aktualizován – je jasnější, rychlejší a méně rušivý."
+ "Aktualizovali jsme vaše zvuky""Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením.""Nastavení obnovy""Nastavení obnovy"
@@ -12,6 +14,8 @@
"Vaše úložiště klíčů není synchronizováno""Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen.""Vylepšete si zážitek z volání"
+ "Všechny chaty"
+ "Prostory""Opravdu chcete odmítnout pozvánku do %1$s?""Odmítnout pozvání""Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"
@@ -21,6 +25,7 @@
"Jedná se o jednorázový proces, prosíme o strpení.""Nastavení vašeho účtu""Vytvořte novou konverzaci nebo místnost"
+ "Vymazat filtry""Začněte tím, že někomu pošnete zprávu.""Zatím žádné konverzace.""Oblíbené"
@@ -30,6 +35,7 @@ Prozatím můžete zrušit výběr filtrů, abyste viděli své další chaty"
"Pozvánky""Nemáte žádné nevyřízené pozvánky.""Nízká priorita"
+ "Zatím nemáte žádné chaty s nízkou prioritou""Můžete zrušit výběr filtrů, abyste viděli své další chaty""Nemáte chaty pro tento výběr""Lidé"
diff --git a/features/home/impl/src/main/res/values-cy/translations.xml b/features/home/impl/src/main/res/values-cy/translations.xml
index 9b44b0fc3fa..c8417952a64 100644
--- a/features/home/impl/src/main/res/values-cy/translations.xml
+++ b/features/home/impl/src/main/res/values-cy/translations.xml
@@ -12,6 +12,8 @@
"Dyw eich allwedd storfa heb ei gydweddu""Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi.""Gwella profiad eich galwadau"
+ "Sgyrsiau"
+ "Gofodau""Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?""Gwrthod y gwahoddiad""Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"
@@ -31,6 +33,7 @@ Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill""Gwahoddiadau""Does gennych chi ddim gwahoddiadau yn aros.""Blaenoriaeth Isel"
+ "Does gennych chi ddim sgyrsiau blaenoriaeth isel eto""Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill""Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn""Pobl"
diff --git a/features/home/impl/src/main/res/values-da/translations.xml b/features/home/impl/src/main/res/values-da/translations.xml
index ae32f7ab667..b0763591aa2 100644
--- a/features/home/impl/src/main/res/values-da/translations.xml
+++ b/features/home/impl/src/main/res/values-da/translations.xml
@@ -3,6 +3,8 @@
"Deaktiver batterioptimering for denne app for at sikre, at alle notifikationer dukker op.""Deaktivér optimering""Modtager du ikke notifikationer?"
+ "Dit notifikationsping er blevet opdateret – tydeligere, hurtigere og mindre forstyrrende."
+ "Vi har opdateret dine lyde""Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder.""Opsæt gendannelse""Konfigurer gendannelse for at beskytte din konto"
@@ -12,6 +14,8 @@
"Dit nøglelager er ikke synkroniseret""For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst.""Gør din opkaldsoplevelse bedre"
+ "Samtaler"
+ "Grupper""Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?""Afvis invitation""Er du sikker på, at du vil afvise denne private samtale med %1$s?"
@@ -31,9 +35,10 @@ For nu kan du fravælge filtre for at se dine andre samtaler""Invitationer""Du har ingen afventende invitationer.""Lav prioritet"
+ "Du har endnu ingen chats med lav prioritet""Du kan fravælge filtre for at se dine andre samtaler""Du har ingen samtaler til dette valg"
- "Mennesker"
+ "Brugere""Du har ingen DM\'er endnu""Rum""Du er ikke i noget rum endnu"
diff --git a/features/home/impl/src/main/res/values-de/translations.xml b/features/home/impl/src/main/res/values-de/translations.xml
index 7f2613809cd..2502df5fc19 100644
--- a/features/home/impl/src/main/res/values-de/translations.xml
+++ b/features/home/impl/src/main/res/values-de/translations.xml
@@ -3,48 +3,53 @@
"Deaktiviere die Batterieoptimierung für diese App, um sicherzustellen, dass alle Benachrichtigungen empfangen werden.""Optimierung deaktivieren""Kommen die Benachrichtigungen nicht an?"
- "Falls Sie alle vorhandenen Geräte verloren haben, stellen Sie Ihre kryptografische Identität und Ihren Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her."
+ "Dein Benachrichtigungs-Ping wurde aktualisiert – klarer, schneller und weniger störend."
+ "Wir haben deine Sounds aktualisiert"
+ "Stelle Deine kryptographische Identität und Deinen Nachrichtenverlauf mit Hilfe eines Wiederherstellungsschlüssels wieder her, falls du alle deine Geräte verloren haben solltest""Wiederherstellung einrichten""Wiederherstellung einrichten"
- "Bestätigen Sie die Validität Ihres Wiederherstellungsschlüssels, um weiterhin auf Ihren Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können."
- "Geben Sie Ihren Wiederherstellungsschlüssel ein"
- "Haben Sie Ihren Wiederherstellungsschlüssel vergessen?"
- "Ihr Schlüsselspeicher ist nicht synchronisiert"
- "Damit Sie keine wichtigen Anrufe verpassen, ändern Sie bitte Ihre Einstellungen, so dass das gesperrte Telefon auch Benachrichtigungen im Vollbildmodus erhalten darf."
+ "Bestätige deinen Wiederherstellungsschlüssel, um weiterhin auf deinen Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können."
+ "Gib deinen Wiederherstellungsschlüssel ein"
+ "Hast du deinen Wiederherstellungsschlüssel vergessen?"
+ "Dein Schlüsselspeicher ist nicht synchronisiert"
+ "Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst.""Verbessere dein Anruferlebnis"
- "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"
+ "Chats"
+ "Spaces"
+ "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?""Einladung ablehnen"
- "Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?"
+ "Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?""Einladung ablehnen""Keine Einladungen""%1$s (%2$s) hat dich eingeladen""Dies ist ein einmaliger Vorgang, danke fürs Warten.""Dein Konto wird eingerichtet."
- "Eine Unterthaltung oder Raum erstellen"
+ "Einen neuen Chat erstellen""Filter zurücksetzen"
- "Beginnen Sie, indem Sie jemandem eine Nachricht senden."
+ "Schick einfach jemandem eine Nachricht, um loszulegen.""Noch keine Chats.""Favoriten"
- "In den Chatroomeinstellungen können Sie einen Chatroom als Favorit markieren.
-Deaktivieren Sie den entsprechenden Filter, um Ihre anderen Chatrooms zu sehen"
- "Sie haben noch keine Chatrooms als Favorit markiert"
+ "In den Chat Einstellungen kannst du einen Chat als Favorit markieren.
+Deaktiviere den entsprechenden Filter, um deine anderen Chats zu sehen"
+ "Du hast noch keine Chats als Favorit markiert""Einladungen"
- "Sie haben keine ausstehenden Einladungen."
+ "Du hast keine ausstehenden Einladungen.""Niedrige Priorität"
- "Wähle Filter ab, um Deine Chats zu sehen."
- "Diese Chats entsprechen diesen Kriterien nicht."
+ "Du hast noch keine Chats mit niedriger Priorität."
+ "Wähle Filter ab, um Chats zu sehen."
+ "Für diese Auswahl hast du keinen Chat.""Personen"
- "Sie haben noch keine Direktnachrichten"
- "Räume"
- "Sie sind noch in keinem Raum"
+ "Du hast noch keine Direktnachrichten"
+ "Gruppen"
+ "Du bist noch in keinem Chat""Ungelesen""Glückwunsch!
-Sie haben keine ungelesenen Nachrichten!"
+Du hast keine ungelesenen Nachrichten!""Beitrittsanfrage geschickt""Chats""Als gelesen markieren""Als ungelesen markieren"
- "Dieser Raum wurde aktualisiert."
- "Sie verwenden anscheinend ein neues Gerät. Verifizieren Sie es mit einem anderen Gerät, um Zugriff auf ihre verschlüsselten Nachrichten zu erhalten."
- "Bestätigen Sie ihre Identität"
+ "Die Chat-Version wurde aktualisiert"
+ "Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst."
+ "Verifiziere deine Identität"
diff --git a/features/home/impl/src/main/res/values-el/translations.xml b/features/home/impl/src/main/res/values-el/translations.xml
index 692aace0ab2..df492840b02 100644
--- a/features/home/impl/src/main/res/values-el/translations.xml
+++ b/features/home/impl/src/main/res/values-el/translations.xml
@@ -12,6 +12,7 @@
"Ο χώρος αποθήκευσης κλειδιών σου δεν είναι συγχρονισμένος""Για να διασφαλίσετε ότι δεν θα χάσετε ποτέ μια σημαντική κλήση, αλλάξτε τις ρυθμίσεις σας ώστε να επιτρέπονται οι ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σας είναι κλειδωμένο.""Βελτίωσε την εμπειρία κλήσεων"
+ "Συνομιλίες""Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;""Απόρριψη πρόσκλησης""Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"
diff --git a/features/home/impl/src/main/res/values-en-rUS/translations.xml b/features/home/impl/src/main/res/values-en-rUS/translations.xml
index 365a3cd3dab..fe1100aa329 100644
--- a/features/home/impl/src/main/res/values-en-rUS/translations.xml
+++ b/features/home/impl/src/main/res/values-en-rUS/translations.xml
@@ -1,5 +1,7 @@
+ "Disable battery optimization for this app, to make sure all notifications are received."
+ "Disable optimization""Favorites""You can add a chat to your favorites in the chat settings.
For now, you can deselect filters in order to see your other chats"
diff --git a/features/home/impl/src/main/res/values-es/translations.xml b/features/home/impl/src/main/res/values-es/translations.xml
index a58f9e743ae..77e52fc6b61 100644
--- a/features/home/impl/src/main/res/values-es/translations.xml
+++ b/features/home/impl/src/main/res/values-es/translations.xml
@@ -9,6 +9,7 @@
"Tu almacén de claves no está sincronizado""Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado.""Mejora tu experiencia de llamada"
+ "Chats""¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?""Rechazar la invitación""¿Estás seguro de que quieres rechazar este chat privado con %1$s?"
diff --git a/features/home/impl/src/main/res/values-et/translations.xml b/features/home/impl/src/main/res/values-et/translations.xml
index f8dd7fc086a..d5b6fa06d45 100644
--- a/features/home/impl/src/main/res/values-et/translations.xml
+++ b/features/home/impl/src/main/res/values-et/translations.xml
@@ -3,6 +3,8 @@
"Kui tahad olla kindel, et näed õigel ajal kõiki teavitusi, siis palun lülita akukasutuse optimeerimine välja.""Lülita akukasutuse optimeerimine välja""Sa ei näe kõiki teavitusi?"
+ "Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."
+ "Oleme sinu helisid värskendanud""Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele.""Seadista andmete taastamine""Seadista taastamine"
@@ -12,6 +14,8 @@
"Sinu võtmehoidla pole sünkroonis""Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused.""Sinu tõhusad telefonikõned"
+ "Vestlused"
+ "Kogukonnad""Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?""Lükka kutse tagasi""Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"
@@ -31,6 +35,7 @@ Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid""Kutsed""Sul pole ootel kutseid.""Vähetähtis"
+ "Sul pole veel ühtegi olulist vestlust""Oma teiste vestluste nägemiseks sa pead filtrid eemaldama""Selle valiku jaoks sul veel pole vestlusi""Inimesed"
diff --git a/features/home/impl/src/main/res/values-eu/translations.xml b/features/home/impl/src/main/res/values-eu/translations.xml
index 8289dc97d47..b1eaf7749db 100644
--- a/features/home/impl/src/main/res/values-eu/translations.xml
+++ b/features/home/impl/src/main/res/values-eu/translations.xml
@@ -1,10 +1,14 @@
+ "Desgaitu bateriaren optimizazioa aplikazio honentzat, ziurtatzeko jakinarazpen guztiak jasoko direla."
+ "Desgaitu optimizazioa"
+ "Jakinarazpenak ez dira iristen?""Konfiguratu berreskurapena""Sartu zure berreskuratze-gakoa""Berreskuratze-gakoa ahaztu al duzu?""Dei garrantzitsurik galduko ez duzula ziurtatzeko, aldatu ezarpenak telefonoa blokeatuta dagoenean pantaila osoko jakinarazpenak baimentzeko.""Hobetu deien esperientzia"
+ "Txatak""Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?""Baztertu gonbidapena""Ziur %1$s(r)en txat pribatua baztertu nahi duzula?"
@@ -14,6 +18,7 @@
"Behin egin beharreko prozesua da; eskerrik asko itxaroteagatik.""Zure kontua konfiguratzen.""Sortu elkarrizketa edo gela berria"
+ "Garbitu iragazkiak""Hasi norbaiti mezuak bidaltzen.""Oraindik ez dago txatik.""Gogokoak"
diff --git a/features/home/impl/src/main/res/values-fa/translations.xml b/features/home/impl/src/main/res/values-fa/translations.xml
index ef436cbe90d..7e3658e0eb7 100644
--- a/features/home/impl/src/main/res/values-fa/translations.xml
+++ b/features/home/impl/src/main/res/values-fa/translations.xml
@@ -1,10 +1,16 @@
+ "از کار انداختن بهینهسازی باتری برای این کاره برای اطمینان از گرفتن همهٔ آگاهیها."
+ "از کار انداختن بهینه سازی"
+ "آگاهیها نمیرسند؟"
+ "بازگردانی تاریخچهٔ پیامها و هویت رمزنگاشتهتان با کلید بازیابی در صورت از دست دادن همهٔ افزارههای موجودتان.""برپایی بازیابی""برپایی بازیابی""ورود کلید بازیابیتان""ذخیرهساز کلیدتان از همگام بودن در آمده""بهبود تجریهٔ تماستان"
+ "گپها"
+ "فضاها""مطمئنید که میخواهید دعوت پیوستن به %1$s را رد کنید؟""رد دعوت""مطمئنید که میخواهید این گپ خصوصی با %1$s را رد کنید؟"
@@ -14,6 +20,7 @@
"فرایندی یک باره است. ممنون از شکیباییتان.""برپایی حسابتان.""ایجاد اتاق یا گفتوگویی جدید"
+ "پاک کردن پالایهها""آغاز با پیام دادن به کسی.""هنوز گپی وجود ندارد.""علاقهمندیها"
@@ -34,6 +41,7 @@
"گپها""علامتگذاری به عنوان خوانده شده""نشان به ناخوانده"
- "به نظر می رسد از دستگاه جدیدی استفاده می کنید. برای دسترسی به پیام های رمزگذاری شده خود، با دستگاه دیگری این دستگاه را تأیید کنید."
+ "این اتاق ارتقا یافته"
+ "گویا از افزارهای جدید استفاده میکنید. تأیید با افزارهای دیگر برای دسترسی به پیامهای رمزنگاری شدهتان.""تأیید کنید که خودتانید"
diff --git a/features/home/impl/src/main/res/values-fi/translations.xml b/features/home/impl/src/main/res/values-fi/translations.xml
index 10da41c88b1..fa7150b5461 100644
--- a/features/home/impl/src/main/res/values-fi/translations.xml
+++ b/features/home/impl/src/main/res/values-fi/translations.xml
@@ -1,5 +1,10 @@
+ "Ota tämän sovelluksen akunkäytön optimointi pois käytöstä varmistaaksesi, että kaikki ilmoitukset tulevat perille."
+ "Ota optimointi pois käytöstä"
+ "Eikö ilmoitukset tule perille?"
+ "Ilmoitusääni on päivitetty — selkeämpi, nopeampi ja vähemmän häiritsevä."
+ "Olemme päivittäneet äänesi""Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi.""Ota palautus käyttöön""Ota palautus käyttöön tilisi suojaamiseksi"
@@ -9,6 +14,8 @@
"Avainten säilytys ei ole synkronoitu""Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua.""Paranna puhelukokemustasi"
+ "Keskustelut"
+ "Tilat""Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?""Hylkää kutsu""Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"
@@ -18,6 +25,7 @@
"Tämä on kertaluonteinen prosessi, kiitos odottamisesta.""Tiliä määritetään.""Luo uusi keskustelu tai huone"
+ "Tyhjennä suodattimet""Aloita lähettämällä viesti jollekin.""Sinulla ei ole vielä keskusteluja.""Suosikit"
@@ -27,6 +35,7 @@ Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."<
"Kutsut""Sinulla ei ole yhtään odottavaa kutsua.""Matala prioriteetti"
+ "Sinulla ei ole vielä yhtään matalan prioriteetin keskustelua""Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi.""Sinulla ei ole sopivia keskusteluja tähän valintaan""Ihmiset"
diff --git a/features/home/impl/src/main/res/values-fr/translations.xml b/features/home/impl/src/main/res/values-fr/translations.xml
index 3f86a648105..d54e8fb943e 100644
--- a/features/home/impl/src/main/res/values-fr/translations.xml
+++ b/features/home/impl/src/main/res/values-fr/translations.xml
@@ -3,6 +3,8 @@
"Désactivez l’optimisation de la batterie pour cette application afin de vous assurer que toutes les notifications sont reçues.""Désactiver l’optimisation""Ils vous manque des notifications?"
+ "Le son des notifications a été modifié: plus clair, plus court et moins perturbateur."
+ "Nous avons rafraîchi les sons""Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l’historique de vos messages chiffrés au cas où vous perdriez l’accès à vos appareils.""Configurer la sauvegarde""Configurer la récupération"
@@ -12,6 +14,8 @@
"Le stockage de vos clés n’est pas synchronisé""Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé.""Améliorez votre expérience d’appel"
+ "Conversations"
+ "Espaces""Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?""Refuser l’invitation""Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"
@@ -31,6 +35,7 @@ En attendant, vous pouvez désélectionner des filtres pour voir vos autres salo
"Invitations""Vous n’avez aucune invitation en attente.""Priorité basse"
+ "Vous n’avez pas encore de salon à priorité basse""Veuillez désélectionner des filtres pour voir vos discussions""Vous n’avez pas de discussions pour cette sélection""Personnes"
diff --git a/features/home/impl/src/main/res/values-hu/translations.xml b/features/home/impl/src/main/res/values-hu/translations.xml
index e0763e4c6fd..cca927910b9 100644
--- a/features/home/impl/src/main/res/values-hu/translations.xml
+++ b/features/home/impl/src/main/res/values-hu/translations.xml
@@ -1,8 +1,10 @@
- "Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést."
+ "Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést.""Optimalizálás letiltása""Nem érkeznek meg az értesítések?"
+ "Értesítési hangja frissült – tisztább, gyorsabb és kevésbé zavaró lett."
+ "Frissítettük a hangokat""Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést.""Helyreállítás beállítása""Helyreállítás beállítása a fiókja védelméhez"
@@ -12,6 +14,8 @@
"A kulcstároló nincs szinkronizálva""Hogy sose maradjon le egyetlen fontos hívásról sem, a beállításokban engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van.""Fokozza a hívásélményét"
+ "Összes csevegés"
+ "Terek""Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?""Meghívás elutasítása""Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"
@@ -31,6 +35,7 @@ Egyelőre törölheti a szűrőket a többi csevegés megtekintéséhez.""Meghívások"
"Nincsenek függőben lévő meghívásai.""Alacsony prioritás"
+ "Még nincsenek alacsony prioritású csevegései""Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez""Ehhez a kiválasztáshoz nem tartoznak csevegések""Emberek"
diff --git a/features/home/impl/src/main/res/values-in/translations.xml b/features/home/impl/src/main/res/values-in/translations.xml
index b3e16b4c25a..924b0191037 100644
--- a/features/home/impl/src/main/res/values-in/translations.xml
+++ b/features/home/impl/src/main/res/values-in/translations.xml
@@ -12,6 +12,7 @@
"Penyimpanan kunci Anda tidak sinkron""Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci.""Tingkatkan pengalaman panggilan Anda"
+ "Semua Obrolan""Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?""Tolak undangan""Apakah Anda yakin ingin menolak obrolan pribadi dengan %1$s?"
diff --git a/features/home/impl/src/main/res/values-it/translations.xml b/features/home/impl/src/main/res/values-it/translations.xml
index 88ff0eb3be4..74f8d4abaca 100644
--- a/features/home/impl/src/main/res/values-it/translations.xml
+++ b/features/home/impl/src/main/res/values-it/translations.xml
@@ -3,7 +3,7 @@
"Disabilita l\'ottimizzazione della batteria per questa app, per assicurarti che tutte le notifiche vengano ricevute.""Disabilita l\'ottimizzazione""Le notifiche non arrivano?"
- "Genera una nuova chiave di recupero che può essere usata per ripristinare la cronologia dei messaggi crittografati nel caso in cui tu perda l\'accesso ai tuoi dispositivi."
+ "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi.""Configura il recupero""Configura il ripristino""Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi."
@@ -12,6 +12,8 @@
"L\'archiviazione delle chiavi non è sincronizzata""Per non perdere mai una chiamata importante, modifica le impostazioni per consentire le notifiche a schermo intero quando il telefono è bloccato.""Migliora la tua esperienza di chiamata"
+ "Tutte le conversazioni"
+ "Spazi""Vuoi davvero rifiutare l\'invito ad entrare in %1$s?""Rifiuta l\'invito""Vuoi davvero rifiutare questa conversazione privata con %1$s?"
@@ -31,6 +33,7 @@ Per il momento, puoi deselezionare i filtri per vedere le altre conversazioni."<
"Inviti""Non hai nessun invito in sospeso.""Bassa priorità"
+ "Non hai ancora conversazioni a bassa priorità""Puoi deselezionare i filtri per vedere le altre conversazioni.""Non hai conversazioni per questa selezione""Persone"
diff --git a/features/home/impl/src/main/res/values-ka/translations.xml b/features/home/impl/src/main/res/values-ka/translations.xml
index bcedaeac607..1aa5b2d33d9 100644
--- a/features/home/impl/src/main/res/values-ka/translations.xml
+++ b/features/home/impl/src/main/res/values-ka/translations.xml
@@ -3,6 +3,7 @@
"აღდგენის დაყენება""დაადასტურეთ თქვენი აღდგენის გასაღები რათა გქონდეთ წვდომა გასაღებების დამგროვებელთან და შეტყობინებების ისტორიასთან.""თქვენი გასაღების დამგროვებელი არაა სინქრონიზებული"
+ "ჩატები""დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?""მოწვევაზე უარის თქმა""დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"
diff --git a/features/home/impl/src/main/res/values-ko/translations.xml b/features/home/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..de823b9257e
--- /dev/null
+++ b/features/home/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,53 @@
+
+
+ "이 앱의 배터리 최적화를 비활성화하여 모든 알림이 정상적으로 수신되도록 합니다."
+ "최적화 비활성화"
+ "알림이 도착하지 않나요?"
+ "기존의 모든 기기를 분실한 경우 복구 키를 사용하여 암호화된 ID 및 메시지 기록을 복구할 수 있습니다."
+ "복구 설정"
+ "계정을 보호하기 위해 복구를 설정하세요"
+ "키 저장소 및 메시지 기록에 대한 액세스를 유지하려면 복구 키를 확인하세요."
+ "복구 키를 입력하세요"
+ "복구 키를 잊으셨나요?"
+ "귀하의 키 저장소가 동기화되지 않았습니다"
+ "중요한 전화를 놓치지 않으려면 휴대폰이 잠겨 있을 때 전체 화면 알림을 허용하도록 설정을 변경하세요."
+ "통화 경험을 향상시키세요"
+ "채팅"
+ "스페이스"
+ "정말로 %1$s 에 참가하지 않고 초대를 거절하시겠어요?"
+ "초대 거절"
+ "%1$s 와의 비공개 채팅을 정말 거부하시겠습니까?"
+ "채팅 거절"
+ "초대 없음"
+ "%1$s (%2$s) 당신을 초대했습니다"
+ "이 과정은 한 번만 진행됩니다, 기다려 주셔서 감사합니다."
+ "계정 설정하기"
+ "새로운 대화 또는 방 만들기"
+ "필터 지우기"
+ "누군가에게 메시지를 보내어 시작해 보세요."
+ "아직 채팅이 없습니다."
+ "즐겨찾기"
+ "채팅 설정에서 채팅을 즐겨찾기에 추가할 수 있습니다.
+현재는 다른 채팅을 보려면 필터를 선택 해제해야 합니다."
+ "아직 즐겨찾는 채팅이 없습니다."
+ "초대"
+ "보류 중인 초대가 없습니다."
+ "낮은 우선순위"
+ "아직 낮은 우선순위 채팅이 없습니다."
+ "다른 채팅을 보려면 필터 선택을 해제하세요."
+ "이 선택 항목에 대한 채팅이 없습니다."
+ "사람"
+ "아직 DM이 없습니다."
+ "방"
+ "아직 어떤 방에도 있지 않습니다."
+ "읽지 않은 항목"
+ "축하합니다!
+읽지 않은 메시지가 없습니다!"
+ "가입 요청이 전송되었습니다"
+ "채팅"
+ "읽음으로 표시"
+ "읽지 않음으로 표시"
+ "이 방이 업그레이드되었습니다"
+ "새 장치를 사용 중인 것 같습니다. 다른 디바이스로 인증하여 암호화된 메시지에 액세스하세요."
+ "본인인지 확인하세요"
+
diff --git a/features/home/impl/src/main/res/values-lt/translations.xml b/features/home/impl/src/main/res/values-lt/translations.xml
index 37e9003c206..03771f7ae0d 100644
--- a/features/home/impl/src/main/res/values-lt/translations.xml
+++ b/features/home/impl/src/main/res/values-lt/translations.xml
@@ -1,5 +1,6 @@
+ "Pokalbiai""Ar tikrai norite atmesti kvietimą prisijungti prie %1$s?""Atmesti kvietimą""Ar tikrai norite atmesti šį privatų pokalbį su %1$s ?"
diff --git a/features/home/impl/src/main/res/values-nb/translations.xml b/features/home/impl/src/main/res/values-nb/translations.xml
index 2ce6733b8aa..42216442cb7 100644
--- a/features/home/impl/src/main/res/values-nb/translations.xml
+++ b/features/home/impl/src/main/res/values-nb/translations.xml
@@ -3,6 +3,8 @@
"Deaktiver batterioptimalisering for denne appen for å sikre at alle varsler mottas.""Deaktiver optimalisering""Kommer ikke varslene frem?"
+ "Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."
+ "Vi har oppdatert lydene dine""Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter.""Konfigurer gjenoppretting""Konfigurer gjenoppretting for å beskytte kontoen din"
@@ -12,6 +14,8 @@
"Nøkkellagringen din er ikke synkronisert""For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst.""Forbedre samtaleopplevelsen din"
+ "Chatter"
+ "Områder""Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?""Avvis invitasjon""Er du sikker på at du vil avslå denne private chatten med %1$s?"
@@ -21,6 +25,7 @@
"Dette er en engangsprosess, takk for at du venter.""Setter opp kontoen din.""Opprett en ny samtale eller et nytt rom"
+ "Fjern filtre""Kom i gang med å sende meldinger til noen.""Ingen chatter ennå.""Favoritter"
@@ -30,6 +35,7 @@ Inntil videre kan du velge bort filtre for å se de andre chattene dine""Invitasjoner"
"Du har ingen ventende invitasjoner.""Lav prioritet"
+ "Du har ingen lavprioriterte chatter ennå""Du kan velge bort filtre for å se de andre chattene dine""Du har ikke chatter for dette utvalget""Personer"
diff --git a/features/home/impl/src/main/res/values-nl/translations.xml b/features/home/impl/src/main/res/values-nl/translations.xml
index fc207da0167..b6df46e98b8 100644
--- a/features/home/impl/src/main/res/values-nl/translations.xml
+++ b/features/home/impl/src/main/res/values-nl/translations.xml
@@ -1,5 +1,6 @@
+ "Optimalisatie uitschakelen""Herstel je cryptografische identiteit en berichtengeschiedenis met een herstelsleutel voor als je al je bestaande apparaten kwijt bent.""Herstelmogelijkheid instellen""Herstel instellen om je account te beschermen"
@@ -7,6 +8,7 @@
"Je sleutelopslag is niet gesynchroniseerd""Pas je instellingen aan om meldingen op het volledige scherm toe te staan wanneer de telefoon is vergrendeld. Zo mis je nooit een belangrijk gesprek.""Verbeter je gesprekservaring"
+ "Chats""Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?""Uitnodiging weigeren""Weet je zeker dat je deze privéchat met %1$s wilt weigeren?"
diff --git a/features/home/impl/src/main/res/values-pl/translations.xml b/features/home/impl/src/main/res/values-pl/translations.xml
index 8bb236e71a5..73e0b5e52e6 100644
--- a/features/home/impl/src/main/res/values-pl/translations.xml
+++ b/features/home/impl/src/main/res/values-pl/translations.xml
@@ -1,5 +1,10 @@
+ "Wyłącz optymalizację baterii dla tej aplikacji, aby upewnić się, że wszystkie powiadomienia są odbierane."
+ "Wyłącz optymalizację"
+ "Powiadomienia nie dochodzą?"
+ "Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy."
+ "Odświeżyliśmy Twoje dźwięki""Wygeneruj nowy klucz przywracania, którego można użyć do przywrócenia historii wiadomości szyfrowanych w przypadku utraty dostępu do swoich urządzeń.""Skonfiguruj przywracanie""Skonfiguruj przywracanie"
@@ -9,6 +14,8 @@
"Magazyn kluczy nie jest zsynchronizowany""Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu.""Popraw jakość swoich rozmów"
+ "Wszystkie czaty"
+ "Przestrzenie""Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?""Odrzuć zaproszenie""Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"
@@ -18,6 +25,7 @@
"Jest to jednorazowy proces, dziękujemy za czekanie.""Konfigurowanie Twojego konta.""Utwórz nową rozmowę lub pokój"
+ "Wyczyść filtry""Wyślij komuś wiadomość, aby rozpocząć.""Brak czatów.""Ulubione"
@@ -27,6 +35,7 @@ Na razie możesz wyczyścić filtry, aby zobaczyć pozostałe czaty"
"Zaproszenia""Nie masz żadnych oczekujących zaproszeń.""Niski priorytet"
+ "Nie masz jeszcze żadnych czatów o niskim priorytecie""Wyczyść filtry, aby zobaczyć pozostałe czaty""Brak czatów dla podanych kryteriów""Osoby"
@@ -40,6 +49,7 @@ Nie masz żadnych nieprzeczytanych wiadomości!"
"Wszystkie czaty""Oznacz jako przeczytane""Oznacz jako nieprzeczytane"
+ "Ten pokój został ulepszony""Wygląda na to, że używasz nowego urządzenia. Zweryfikuj się innym urządzeniem, aby uzyskać dostęp do zaszyfrowanych wiadomości.""Potwierdź, że to Ty"
diff --git a/features/home/impl/src/main/res/values-pt-rBR/translations.xml b/features/home/impl/src/main/res/values-pt-rBR/translations.xml
index 871a2bcc5cc..7aab508a380 100644
--- a/features/home/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/home/impl/src/main/res/values-pt-rBR/translations.xml
@@ -3,7 +3,7 @@
"Desative a otimização de bateria para este app, para que tenha certeza que todas as notificações sejam recebidas.""Desativar otimização""As notificações não chegam?"
- "Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se você tiver perdido todos os dispositivos existentes."
+ "Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação caso você perda todos os dispositivos existentes.""Configurar a recuperação""Configure a recuperação para proteger sua conta""Confirme sua chave de recuperação para manter o acesso ao seu armazenamento de chaves e histórico de mensagens."
@@ -12,11 +12,13 @@
"Seu armazenamento de chaves está fora de sincronia""Para garantir que você nunca perca uma chamada importante, por favor altere as suas configurações para permitir notificações em tela cheia enquanto o seu celular estiver bloqueado.""Melhore a sua experiência de chamadas"
- "Tem certeza de que deseja recusar o convite para ingressar em %1$s?"
+ "Conversas"
+ "Espaços"
+ "Tem certeza de que deseja recusar o convite para entrar em %1$s?""Recusar convite"
- "Tem certeza de que deseja recusar esse chat privado com %1$s?"
+ "Tem certeza de que deseja recusar esse conversa privada com %1$s?""Recusar chat"
- "Sem convites"
+ "Não há convites""%1$s(%2$s) convidou você""Este é um processo único, obrigado por esperar.""Configurando sua conta."
@@ -25,8 +27,8 @@
"Comece enviando uma mensagem para alguém.""Ainda não há conversas.""Favoritos"
- "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo.
-Por enquanto, você pode desmarcar os filtros para ver seus outros bate-papos"
+ "Você pode adicionar uma conversa aos seus favoritos nas configurações da conversa.
+Por enquanto, você pode desmarcar os filtros para ver suas outras conversas""Você não tem nenhuma conversa favorita ainda""Convites""Você não tem nenhum convite pendente."
@@ -34,16 +36,16 @@ Por enquanto, você pode desmarcar os filtros para ver seus outros bate-papos"
"Você pode desmarcar filtros para ver suas outras conversas""Você não tem conversas para esta seleção""Pessoas"
- "Você não tem nenhum conversa privada ainda"
+ "Você não tem nenhuma conversa privada ainda""Salas""Você não está em nenhuma sala ainda"
- "Não lidos"
+ "Não lidas""Parabéns!
Você não tem nenhuma mensagem não lida!"
- "Pedido de adesão enviado"
+ "Pedido de entrada enviado""Conversas"
- "Marcar como lido"
- "Marcar como não lido"
+ "Marcar como lida"
+ "Marcar como não lida""Esta sala foi atualizada""Parece que você está usando um novo dispositivo. Verifique com outro dispositivo para acessar suas mensagens criptografadas.""Verifique se é você"
diff --git a/features/home/impl/src/main/res/values-pt/translations.xml b/features/home/impl/src/main/res/values-pt/translations.xml
index daba977faac..6c9f5a6fbdd 100644
--- a/features/home/impl/src/main/res/values-pt/translations.xml
+++ b/features/home/impl/src/main/res/values-pt/translations.xml
@@ -12,9 +12,11 @@
"O teu armazenamento de chaves não está sincronizado""Para garantir que nunca perdes uma chamada importante, altera as configurações para permitir notificações em ecrã inteiro quando o telemóvel está bloqueado.""Melhora a tua experiência de chamada"
+ "Conversas"
+ "Espaços""Tens a certeza que queres rejeitar o convite para entra em %1$s?""Rejeitar convite"
- "Tem a certeza que queres rejeitar esta conversa privada com %1$s?"
+ "Tens a certeza que queres rejeitar esta conversa privada com %1$s?""Rejeitar conversa""Sem convites""%1$s (%2$s) convidou-te"
@@ -31,6 +33,7 @@ Por enquanto, podes anular a seleção dos filtros para veres as tuas outras con
"Convites""Não tens nenhum convite pendente.""Prioridade baixa"
+ "Ainda não tens conversas de prioridade baixa""Podes anular a seleção dos filtros para veres as tuas outras conversas""Não tens nenhuma conversa selecionada""Pessoas"
diff --git a/features/home/impl/src/main/res/values-ro/translations.xml b/features/home/impl/src/main/res/values-ro/translations.xml
index 2d7bdd8a952..e4a80b4fb72 100644
--- a/features/home/impl/src/main/res/values-ro/translations.xml
+++ b/features/home/impl/src/main/res/values-ro/translations.xml
@@ -1,12 +1,21 @@
- "Recuperați-vă identitatea criptografică și istoricul mesajelor cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente."
+ "Dezactivați optimizarea bateriei pentru această aplicație, pentru a vă asigura că toate notificările sunt primite."
+ "Dezactivați optimizarea"
+ "Nu primiți notificări?"
+ "Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar."
+ "Am reîmprospătat sunetele"
+ "Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente.""Configurați recuperarea""Configurați recuperarea pentru a vă proteja contul"
- "Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."
+ "Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."
+ "Introduceți cheia de recuperare"
+ "Ați uitat cheia de recuperare?""Backup-ul nu este sincronizat""Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat.""Îmbunătățiți-vă experiența in timpul unui apel"
+ "Toate conversatiile"
+ "Spații""Sigur doriți să refuzați alăturarea la %1$s?""Refuzați invitația""Sigur doriți să refuzați conversațiile cu %1$s?"
@@ -16,6 +25,7 @@
"Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare.""Contul dumneavoastră se configurează""Creați o conversație sau o cameră nouă"
+ "Ștergeți filtrele""Începeți prin a trimite mesaje cuiva.""Nu există încă discuții.""Favorite"
@@ -25,6 +35,7 @@ Deocamdată, puteți deselecta filtrele pentru a vedea celelalte chat-uri""Invitații"
"Nu aveți invitații în așteptare.""Prioritate scăzută"
+ "Nu aveți încă niciun chat cu prioritate scăzută.""Puteți deselecta filtrele pentru a vedea celelalte chat-uri""Nu aveți chat-uri pentru această selecție""Persoane"
@@ -38,6 +49,7 @@ Nu aveți mesaje necitite!"
"Toate conversatiile""Marcați ca citită""Marcați ca necitită"
+ "Această cameră a fost modernizată.""Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate.""Verificați că sunteți dumneavoastră"
diff --git a/features/home/impl/src/main/res/values-ru/translations.xml b/features/home/impl/src/main/res/values-ru/translations.xml
index a70efcfdfdd..a5007784ee6 100644
--- a/features/home/impl/src/main/res/values-ru/translations.xml
+++ b/features/home/impl/src/main/res/values-ru/translations.xml
@@ -1,5 +1,10 @@
+ "Выключите оптимизацию расхода батареи, чтобы убедиться, что все уведомления будут поступать."
+ "Выключить оптимизацию"
+ "Уведомления не поступают?"
+ "Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие."
+ "Мы обновили ваши звуки""Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам.""Настроить восстановление""Для защиты вашего аккаунта рекомендуется настроить восстановление"
@@ -9,6 +14,8 @@
"Хранилище ключей не синхронизировано""Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона.""Улучшите качество звонков"
+ "Все чаты"
+ "Пространства""Вы уверены, что хотите отклонить приглашение в %1$s?""Отклонить приглашение""Вы уверены, что хотите отказаться от личного общения с %1$s?"
@@ -18,6 +25,7 @@
"Это одноразовый процесс, спасибо, что подождали.""Настройка учетной записи.""Создайте новую беседу или комнату"
+ "Очистить фильтры""Начните переписку с отправки сообщения.""Пока нет доступных чатов.""Избранное"
@@ -27,6 +35,7 @@
"Приглашения""У вас нет отложенных приглашений.""Низкий приоритет"
+ "У вас пока нет чатов с низким приоритетом.""Вы можете убрать фильтры, чтобы увидеть другие ваши чаты.""У вас нет чатов для этой подборки""Пользователи"
@@ -35,7 +44,7 @@
"Вас пока нет ни в одной комнате""Непрочитанные""Поздравляем!
-У вас нет непрочитанных сообщений!"
+Все сообщения прочитаны!"
"Запрос на присоединение отправлен""Все чаты""Пометить как прочитанное"
diff --git a/features/home/impl/src/main/res/values-sk/translations.xml b/features/home/impl/src/main/res/values-sk/translations.xml
index ade2c192c7b..275a4824b33 100644
--- a/features/home/impl/src/main/res/values-sk/translations.xml
+++ b/features/home/impl/src/main/res/values-sk/translations.xml
@@ -3,6 +3,8 @@
"Vypnite optimalizáciu batérie pre túto aplikáciu, aby ste sa uistili, že sú prijaté všetky upozornenia.""Zakázať optimalizáciu""Oznámenia neprichádzajú?"
+ "Vaše oznámenia boli aktualizované – sú prehľadnejšie, rýchlejšie a menej rušivé."
+ "Obnovili sme vaše zvuky""Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam.""Nastaviť obnovenie""Nastaviť obnovenie"
@@ -12,6 +14,8 @@
"Vaše úložisko kľúčov nie je synchronizované""Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý.""Vylepšite svoj zážitok z hovoru"
+ "Všetky konverzácie"
+ "Priestory""Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?""Odmietnuť pozvanie""Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"
@@ -31,6 +35,7 @@ Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie"<
"Pozvánky""Nemáte žiadne čakajúce pozvánky.""Nízka priorita"
+ "Zatiaľ nemáte žiadne konverzácie s nízkou prioritou.""Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie""Nemáte konverzácie pre tento výber""Ľudia"
diff --git a/features/home/impl/src/main/res/values-sv/translations.xml b/features/home/impl/src/main/res/values-sv/translations.xml
index ab00fd3dd3a..d92b3511188 100644
--- a/features/home/impl/src/main/res/values-sv/translations.xml
+++ b/features/home/impl/src/main/res/values-sv/translations.xml
@@ -1,5 +1,8 @@
+ "Inaktivera batterioptimering för den här appen för att säkerställa att alla aviseringar tas emot."
+ "Inaktivera optimering"
+ "Aviseringar kommer inte fram?""Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter.""Ställ in återställning""Ställ in återställning"
@@ -9,6 +12,8 @@
"Din nyckellagring är inte synkroniserad""För att säkerställa att du aldrig missar ett viktigt samtal, ändra dina inställningar för att tillåta helskärmsmeddelanden när telefonen är låst.""Förbättra din samtalsupplevelse"
+ "Alla chattar"
+ "Utrymmen""Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?""Avböj inbjudan""Är du säker på att du vill avböja denna privata chatt med %1$s?"
@@ -18,6 +23,7 @@
"Detta är en engångsprocess, tack för att du väntar.""Konfigurerar ditt konto""Skapa en ny konversation eller ett nytt rum"
+ "Rensa filter""Kom igång genom att skicka meddelanden till någon.""Inga chattar än.""Favoriter"
@@ -27,6 +33,7 @@ För tillfället kan du avmarkera filter för att se dina andra chattar""Inbjudningar"
"Du har inga väntande inbjudningar.""Låg prioritet"
+ "Du har inga lågprioriterade chattar ännu""Du kan avmarkera filter för att se dina andra chattar""Du har inga chattar för det här valet""Personer"
diff --git a/features/home/impl/src/main/res/values-tr/translations.xml b/features/home/impl/src/main/res/values-tr/translations.xml
index 6f42809ef32..1dda320ff06 100644
--- a/features/home/impl/src/main/res/values-tr/translations.xml
+++ b/features/home/impl/src/main/res/values-tr/translations.xml
@@ -9,6 +9,7 @@
"Anahtar depolama alanınız senkronize değil""Önemli bir aramayı asla kaçırmamak için, telefonunuz kilitliyken tam ekran bildirimlere izin vermek üzere ayarlarınızı değiştirin.""Arama deneyiminizi geliştirin"
+ "Sohbetler""%1$s katılma davetini reddetmek istediğinizden emin misiniz?""Daveti reddet""%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?"
diff --git a/features/home/impl/src/main/res/values-uk/translations.xml b/features/home/impl/src/main/res/values-uk/translations.xml
index 57d750d0293..f41f077df09 100644
--- a/features/home/impl/src/main/res/values-uk/translations.xml
+++ b/features/home/impl/src/main/res/values-uk/translations.xml
@@ -12,6 +12,8 @@
"Ваше сховище ключів не синхронізовано""Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано.""Покращуйте досвід дзвінків"
+ "Бесіди"
+ "Простори""Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?""Відхилити запрошення""Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"
@@ -21,6 +23,7 @@
"Це одноразовий процес, дякую за очікування.""Налаштування облікового запису.""Створити нову розмову або кімнату"
+ "Очистити фільтри""Почніть з обміну повідомленнями з кимось.""Ще немає бесід.""Обране"
@@ -30,6 +33,7 @@
"Запрошення""У вас немає запрошень, що очікують на розгляд.""Низький пріоритет"
+ "У вас ще немає неважливих бесід""Ви можете зняти фільтри, щоб побачити інші ваші бесіди""Ви не маєте бесід для цієї категорії""Люди"
diff --git a/features/home/impl/src/main/res/values-ur/translations.xml b/features/home/impl/src/main/res/values-ur/translations.xml
index 7a0fd3acb6b..78166c34c78 100644
--- a/features/home/impl/src/main/res/values-ur/translations.xml
+++ b/features/home/impl/src/main/res/values-ur/translations.xml
@@ -1,10 +1,13 @@
+ "اگر آپ اپنے تمام موجودہ آلات کھو چکے ہیں تو ایک recovery key کے ذریعہ اپنی کرپٹوگرافک شناخت اور پیغام کی سرگزشت کو دوبارہ حاصل کریں۔""بازیابی مرتب کریں"
+ "اپنے اکاؤنٹ کی حفاظت کے لیے ریکوری طے کریں""اپنے کلید کے ذخیرہ اور پیغام کی سرگزشت تک رسائی کو برقرار رکھنے کیلئے اپنی بازیابی کلید کی تصدیق کریں۔""آپ کا کلید کا ذخیرہ غیر ہم وقت ساز ہے۔""اس بات کو یقینی بنانے کے لیے کہ آپ کبھی بھی اہم مکالمہ سے محروم نہ ہوں، براہ کرم اپنی ترتیبات تبدیل کریں تاکہ آپ کا ہاتف مقفل ہونے پر مکمل پردۂ نمائش اطلاعات کی اجازت دی جا سکے۔""اپنے مکالمتی تجربے کو احسن کریں"
+ "گفتگوئیں""کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟""دعوت مسترد کریں""کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟"
diff --git a/features/home/impl/src/main/res/values-uz/translations.xml b/features/home/impl/src/main/res/values-uz/translations.xml
index 077f9ecc4ae..fbfeca156ae 100644
--- a/features/home/impl/src/main/res/values-uz/translations.xml
+++ b/features/home/impl/src/main/res/values-uz/translations.xml
@@ -1,6 +1,18 @@
+ "Ushbu ilova uchun quvvatni optimallashtirishni oʻchirib qoʻying, barcha xabarnomalar qabul qilinganligiga ishonch hosil qilish uchun."
+ "Optimallashtirishni o\'chiring"
+ "Bildirishnoma kelmayaptimi?"
+ "Mavjud barcha qurilmalarni yoʻqotgan boʻlsangiz, kriptografik kimligingizni va xabarlar tarixini qayta tiklovchi kalit bilan saqlab qoʻying.""Qayta tiklashni sozlang"
+ "Hisobingizni himoya qilish uchun tiklashni sozlang"
+ "Kalit saqlash joyingiz va xabarlar tarixingizga kirishni saqlab qolish uchun tiklash kalitingizni tasdiqlang."
+ "Qayta tiklash kalitingizni kiriting"
+ "Tiklash kalitini unutdingizmi?"
+ "Kalit saqlash joyi sinxronlashmagan"
+ "Muhim qoʻngʻiroqlarni oʻtkazib yubormasligingiz uchun telefoningiz qulflangan holatida toʻliq ekranli bildirishnomalarni ko‘rsatishga ruxsat beradigan qilib sozlamalaringizni oʻzgartiring."
+ "Qoʻngʻiroq tajribangizni yaxshilang"
+ "Suhbatlar""Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?""Taklifni rad etish""Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?"
@@ -12,9 +24,26 @@
"Yangi suhbat yoki xona yarating""Kimgadir xabar yuborishdan boshlang.""Hozircha chatlar yo‘q."
+ "Sevimlilar"
+ "Siz chat sozlamalarida suhbatni sevimlilar ro‘yxatiga qo‘shishingiz mumkin.
+Hozircha, boshqa suhbatlaringizni ko‘rish uchun filtrlarni bekor qilishingiz mumkin."
+ "Sizda hali sevimli chatlar yo‘q""Takliflar"
+ "Sizda hech qanday kutilayotgan takliflar yoʻq."
+ "Past darajali"
+ "Boshqa suhbatlaringizni koʻrish uchun filtrlarni bekor qilishingiz mumkin"
+ "Sizda bu tanlov uchun chatlar yo‘q""Odamlar"
+ "Sizda hali hech qanday shaxsiy xabarlar yo‘q"
+ "Xonalar"
+ "Hali hech qaysi xonada emassiz"
+ "Oʻqilmaganlar"
+ "Tabriklaymiz!
+Sizda oʻqilmagan xabarlar yoʻq!"
+ "Qo‘shilish so‘rovi yuborildi""Suhbatlar"
+ "Oʻqilgan deb belgilash"
+ "Oʻqilmagan deb belgilash""Siz yangi qurilmadan foydalanayotganga o‘xshaysiz. Shifrlangan xabarlaringizga kirish uchun boshqa qurilma bilan tasdiqlang.""Siz ekanligingizni tasdiqlang"
diff --git a/features/home/impl/src/main/res/values-zh-rTW/translations.xml b/features/home/impl/src/main/res/values-zh-rTW/translations.xml
index 1bc2ae181c5..20ee59da98d 100644
--- a/features/home/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/home/impl/src/main/res/values-zh-rTW/translations.xml
@@ -1,5 +1,10 @@
+ "停用此應用程式的電池最佳化,才能確保收到所有通知。"
+ "停用最佳化"
+ "沒收到通知?"
+ "您的通知提示音已更新,更清晰、更快、更不易分心。"
+ "我們已更新您的音效設定""若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。""設定復原""設定備援以保護您的帳號"
@@ -9,6 +14,8 @@
"您的金鑰儲存空間並未同步""為確保您永遠不會錯過重要通話,請變更設定以允許在手機鎖定時允許全螢幕通知。""提升您的通話體驗"
+ "所有聊天室"
+ "空間""您確定您想要拒絕加入 %1$s 的邀請嗎?""拒絕邀請""您確定您要拒絕此與 %1$s 的私人聊天嗎?"
@@ -18,6 +25,7 @@
"這是一次性的程序,感謝您耐心等候。""正在設定您的帳號。""建立新的對話或聊天室"
+ "清除篩選條件""從向某人傳送訊息開始。""尚無聊天室。""我的最愛"
@@ -27,6 +35,7 @@
"邀請""您沒有任何擱置中的邀請。""低優先度"
+ "您尚無任何低優先程度聊天""您可以取消選取篩選條件以檢視其他聊天""您並無此選擇的聊天""夥伴"
@@ -40,6 +49,7 @@
"所有聊天室""標為已讀""標為未讀"
+ "此聊天室已升級""您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。""驗證這是您本人"
diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml
index 4ca91cb7968..805b56de5cd 100644
--- a/features/home/impl/src/main/res/values-zh/translations.xml
+++ b/features/home/impl/src/main/res/values-zh/translations.xml
@@ -1,5 +1,6 @@
+ "请关闭本应用的电池优化设置,确保不错过任何消息通知。""禁用优化""通知未送达?""生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"
@@ -11,6 +12,8 @@
"你的密钥存储已不同步""为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。""提升通话体验"
+ "全部聊天"
+ "空间""您确定要拒绝加入 %1$s 的邀请吗?""拒绝邀请""您确定要拒绝与 %1$s 开始私聊吗?"
@@ -20,6 +23,7 @@
"这是一个一次性的过程,感谢您的等待。""设置您的账户。""创建新的对话或聊天室"
+ "清除筛选条件""通过向某人发送消息来开始。""还没有聊天。""收藏夹"
@@ -29,6 +33,7 @@
"邀请""没有待处理的邀请。""低优先级"
+ "您还没有任何低优先级聊天""您可以取消选择过滤器以查看其他对话""您没有关于此选项的聊天""用户"
@@ -42,6 +47,7 @@
"全部聊天""标记为已读""标记为未读"
+ "此房间已升级""您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。""验证是你本人"
diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml
index 58afb065915..80a263a1794 100644
--- a/features/home/impl/src/main/res/values/localazy.xml
+++ b/features/home/impl/src/main/res/values/localazy.xml
@@ -1,8 +1,10 @@
- "Disable battery optimization for this app, to make sure all notifications are received."
- "Disable optimization"
+ "Disable battery optimisation for this app, to make sure all notifications are received."
+ "Disable optimisation""Notifications not arriving?"
+ "Your notification ping has been updated—clearer, quicker, and less disruptive."
+ "We’ve refreshed your sounds""Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices.""Set up recovery""Set up recovery to protect your account"
@@ -12,6 +14,8 @@
"Your key storage is out of sync""To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked.""Enhance your call experience"
+ "Chats"
+ "Spaces""Are you sure you want to decline the invitation to join %1$s?""Decline invite""Are you sure you want to decline this private chat with %1$s?"
@@ -31,6 +35,7 @@ For now, you can deselect filters in order to see your other chats"
"Invites""You don\'t have any pending invites.""Low Priority"
+ "You don’t have any low priority chats yet""You can deselect filters in order to see your other chats""You don’t have chats for this selection""People"
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt
new file mode 100644
index 00000000000..13c95853864
--- /dev/null
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/CurrentUserWithNeighborsBuilderTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.A_USER_ID_2
+import io.element.android.libraries.matrix.test.A_USER_ID_3
+import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.sessionstorage.api.SessionData
+import io.element.android.libraries.sessionstorage.test.aSessionData
+import org.junit.Test
+
+class CurrentUserWithNeighborsBuilderTest {
+ @Test
+ fun `build on empty list returns current user`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser()
+ val list = listOf()
+ val result = sut.build(matrixUser, list)
+ assertThat(result).containsExactly(matrixUser)
+ }
+
+ @Test
+ fun `ensure that account are sorted by position`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ position = 3,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ position = 2,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_3.value,
+ position = 1,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID_3,
+ A_USER_ID_2,
+ A_USER_ID,
+ )
+ }
+
+ @Test
+ fun `if current user is not found, return a singleton with current user`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_3.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID,
+ )
+ }
+
+ @Test
+ fun `one account, will return a singleton`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID,
+ )
+ }
+
+ @Test
+ fun `two accounts, first is current, will return 3 items`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID_2,
+ A_USER_ID,
+ A_USER_ID_2,
+ )
+ }
+
+ @Test
+ fun `two accounts, second is current, will return 3 items`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID,
+ A_USER_ID_2,
+ A_USER_ID,
+ )
+ }
+
+ @Test
+ fun `three accounts, first is current, will return last current and next`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_3.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID_3,
+ A_USER_ID,
+ A_USER_ID_2,
+ )
+ }
+
+ @Test
+ fun `three accounts, second is current, will return first current and last`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_3.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID,
+ A_USER_ID_2,
+ A_USER_ID_3,
+ )
+ }
+
+ @Test
+ fun `three accounts, current is last, will return middle, current and first`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(id = A_USER_ID_3.value)
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID_2.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID_3.value,
+ ),
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result.map { it.userId }).containsExactly(
+ A_USER_ID,
+ A_USER_ID_2,
+ A_USER_ID_3,
+ )
+ }
+
+ @Test
+ fun `one account, will return data from matrix user and not from db`() {
+ val sut = CurrentUserWithNeighborsBuilder()
+ val matrixUser = aMatrixUser(
+ id = A_USER_ID.value,
+ displayName = "Bob",
+ avatarUrl = "avatarUrl",
+ )
+ val list = listOf(
+ aSessionData(
+ sessionId = A_USER_ID.value,
+ userDisplayName = "Outdated Bob",
+ userAvatarUrl = "outdatedAvatarUrl",
+ ),
+ )
+ val result = sut.build(matrixUser, list)
+ assertThat(result).containsExactly(
+ MatrixUser(
+ userId = A_USER_ID,
+ displayName = "Bob",
+ avatarUrl = "avatarUrl",
+ )
+ )
+ }
+}
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt
new file mode 100644
index 00000000000..ed6482b0e71
--- /dev/null
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.home.api.HomeEntryPoint
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DefaultHomeEntryPointTest {
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultHomeEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ HomeFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ matrixClient = FakeMatrixClient(),
+ presenter = createHomePresenter(),
+ inviteFriendsUseCase = { lambdaError() },
+ analyticsService = FakeAnalyticsService(),
+ acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
+ directLogoutView = { _ -> lambdaError() },
+ reportRoomEntryPoint = { _, _, _ -> lambdaError() },
+ declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
+ changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
+ leaveRoomRenderer = { _, _, _ -> lambdaError() },
+ )
+ }
+ val callback = object : HomeEntryPoint.Callback {
+ override fun navigateToRoom(roomId: RoomId) = lambdaError()
+ override fun navigateToCreateRoom() = lambdaError()
+ override fun navigateToSettings() = lambdaError()
+ override fun navigateToSetUpRecovery() = lambdaError()
+ override fun navigateToEnterRecoveryKey() = lambdaError()
+ override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
+ override fun navigateToBugReport() = lambdaError()
+ }
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ callback = callback,
+ )
+ assertThat(result).isInstanceOf(HomeFlowNode::class.java)
+ assertThat(result.plugins).contains(callback)
+ }
+}
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt
index 6cbebb3135e..3e88ff76415 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt
index 6913ea19bf6..0ae3ea1ff37 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,9 +12,15 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.aRoomListState
+import io.element.android.features.home.impl.spaces.HomeSpacesState
+import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
+import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
+import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -28,10 +35,17 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
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.core.aBuildMeta
import io.element.android.libraries.matrix.test.sync.FakeSyncService
+import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
+import io.element.android.libraries.sessionstorage.test.aSessionData
+import io.element.android.tests.testutils.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -40,6 +54,8 @@ class HomePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
+ private val isSpaceEnabled = FeatureFlags.Space.defaultValue(aBuildMeta())
+
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val matrixClient = FakeMatrixClient(
@@ -49,20 +65,52 @@ class HomePresenterTest {
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
val presenter = createHomePresenter(
client = matrixClient,
- rageshakeFeatureAvailability = { false },
+ rageshakeFeatureAvailability = { flowOf(false) },
+ sessionStore = InMemorySessionStore(
+ initialList = listOf(
+ aSessionData(
+ sessionId = matrixClient.sessionId.value,
+ userDisplayName = null,
+ userAvatarUrl = null,
+ )
+ ),
+ ),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
- assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
+ assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
+ MatrixUser(A_USER_ID, null, null)
+ )
assertThat(initialState.canReportBug).isFalse()
+ skipItems(1)
val withUserState = awaitItem()
- assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
- assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
- assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
+ assertThat(withUserState.currentUserAndNeighbors.first()).isEqualTo(
+ MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
+ )
assertThat(withUserState.showAvatarIndicator).isFalse()
- assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
+ assertThat(withUserState.isSpaceFeatureEnabled).isEqualTo(isSpaceEnabled)
+ assertThat(withUserState.showNavigationBar).isEqualTo(isSpaceEnabled)
+ }
+ }
+
+ @Test
+ fun `present - can report bug`() = runTest {
+ val presenter = createHomePresenter(
+ rageshakeFeatureAvailability = { flowOf(true) },
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
+ )
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.canReportBug).isFalse()
+ val finalState = awaitItem()
+ assertThat(finalState.canReportBug).isTrue()
}
}
@@ -72,6 +120,9 @@ class HomePresenterTest {
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.Space.key to true),
),
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
)
presenter.test {
skipItems(1)
@@ -85,10 +136,14 @@ class HomePresenterTest {
val indicatorService = FakeIndicatorService()
val presenter = createHomePresenter(
indicatorService = indicatorService,
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
assertThat(initialState.showAvatarIndicator).isFalse()
indicatorService.setShowRoomListTopBarIndicator(true)
@@ -104,45 +159,102 @@ class HomePresenterTest {
userAvatarUrl = null,
)
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
- val presenter = createHomePresenter(client = matrixClient)
+ val presenter = createHomePresenter(
+ client = matrixClient,
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
- assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
+ assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
// No new state is coming
}
}
@Test
fun `present - NavigationBar change`() = runTest {
- val presenter = createHomePresenter()
+ val showAnnouncementResult = lambdaRecorder { }
+ val presenter = createHomePresenter(
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
+ announcementService = FakeAnnouncementService(
+ showAnnouncementResult = showAnnouncementResult,
+ )
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
+ if (isSpaceEnabled) skipItems(1)
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
+ showAnnouncementResult.assertions().isCalledOnce()
+ .with(value(Announcement.Space))
}
}
- private fun TestScope.createHomePresenter(
- client: MatrixClient = FakeMatrixClient(),
- syncService: SyncService = FakeSyncService(),
- snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
- rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
- indicatorService: IndicatorService = FakeIndicatorService(),
- featureFlagService: FeatureFlagService = FakeFeatureFlagService()
- ) = HomePresenter(
- client = client,
- syncService = syncService,
- snackbarDispatcher = snackbarDispatcher,
- indicatorService = indicatorService,
- logoutPresenter = { aDirectLogoutState() },
- roomListPresenter = { aRoomListState() },
- rageshakeFeatureAvailability = rageshakeFeatureAvailability,
- featureFlagService = featureFlagService,
- )
+ @Test
+ fun `present - NavigationBar is hidden when the last space is left`() = runTest {
+ val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
+ val presenter = createHomePresenter(
+ sessionStore = InMemorySessionStore(
+ updateUserProfileResult = { _, _, _ -> },
+ ),
+ featureFlagService = FakeFeatureFlagService(
+ initialState = mapOf(FeatureFlags.Space.key to true),
+ ),
+ homeSpacesPresenter = homeSpacesPresenter,
+ announcementService = FakeAnnouncementService(
+ showAnnouncementResult = {},
+ )
+ )
+ presenter.test {
+ skipItems(1)
+ val initialState = awaitItem()
+ assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
+ assertThat(initialState.showNavigationBar).isTrue()
+ // User navigate to Spaces
+ initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
+ val spaceState = awaitItem()
+ assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
+ // The last space is left
+ homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList()))
+ skipItems(1)
+ val finalState = awaitItem()
+ // We are back to Chats
+ assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
+ assertThat(finalState.showNavigationBar).isFalse()
+ }
+ }
}
+
+internal fun createHomePresenter(
+ client: MatrixClient = FakeMatrixClient(),
+ syncService: SyncService = FakeSyncService(),
+ snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
+ rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
+ indicatorService: IndicatorService = FakeIndicatorService(),
+ featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
+ homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() },
+ sessionStore: SessionStore = InMemorySessionStore(),
+ announcementService: AnnouncementService = FakeAnnouncementService(),
+) = HomePresenter(
+ client = client,
+ syncService = syncService,
+ snackbarDispatcher = snackbarDispatcher,
+ indicatorService = indicatorService,
+ logoutPresenter = { aDirectLogoutState() },
+ roomListPresenter = { aRoomListState() },
+ homeSpacesPresenter = homeSpacesPresenter,
+ rageshakeFeatureAvailability = rageshakeFeatureAvailability,
+ featureFlagService = featureFlagService,
+ sessionStore = sessionStore,
+ announcementService = announcementService,
+)
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt
index 03fe2efd942..35c30af86eb 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt
index 95444b6c4f3..fd51c3b53eb 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt
index 3f4f45afaeb..250f43ee8f9 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt
index a71afca607f..df1bc181076 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt
index 1fd37d92528..9e992208856 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
index 55b3f1ffee3..72465e15fdc 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
class RoomListBaseRoomSummaryTest {
@@ -85,6 +86,7 @@ internal fun createRoomListRoomSummary(
heroes: List = emptyList(),
timestamp: String? = null,
isTombstoned: Boolean = false,
+ isSpace: Boolean = false,
) = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
@@ -104,6 +106,7 @@ internal fun createRoomListRoomSummary(
canonicalAlias = null,
inviteSender = null,
isDm = false,
- heroes = heroes.toPersistentList(),
+ heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
+ isSpace = isSpace
)
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt
index b336178d22d..b692d49d1ef 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -73,7 +74,7 @@ class RoomListContextMenuTest {
eventsRecorder.assertList(
listOf(
RoomListEvents.HideContextMenu,
- RoomListEvents.LeaveRoom(contextMenu.roomId),
+ RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true),
)
)
}
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt
index 48a51b87bb0..0803993a0b4 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
index bc0163585f1..cf535e187d8 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,6 +13,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
+import io.element.android.features.announcement.api.Announcement
+import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.FakeDateTimeObserver
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
@@ -24,17 +27,16 @@ import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
+import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
-import io.element.android.features.leaveroom.api.aLeaveRoomState
+import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
-import io.element.android.libraries.featureflag.api.FeatureFlagService
-import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@@ -77,6 +79,7 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -218,7 +221,6 @@ class RoomListPresenterTest {
roomName = summary.name,
isDm = false,
isFavorite = false,
- markAsUnreadFeatureFlagEnabled = true,
hasNewContent = false,
displayClearRoomCacheAction = false,
)
@@ -236,7 +238,6 @@ class RoomListPresenterTest {
roomName = summary.name,
isDm = false,
isFavorite = true,
- markAsUnreadFeatureFlagEnabled = true,
hasNewContent = false,
displayClearRoomCacheAction = false,
)
@@ -264,7 +265,6 @@ class RoomListPresenterTest {
roomName = summary.name,
isDm = false,
isFavorite = false,
- markAsUnreadFeatureFlagEnabled = true,
// true here.
hasNewContent = false,
displayClearRoomCacheAction = true,
@@ -296,7 +296,6 @@ class RoomListPresenterTest {
roomName = summary.name,
isDm = false,
isFavorite = false,
- markAsUnreadFeatureFlagEnabled = true,
hasNewContent = false,
displayClearRoomCacheAction = false,
)
@@ -319,8 +318,8 @@ class RoomListPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
- leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
+ initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
+ leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
cancelAndIgnoreRemainingEvents()
}
}
@@ -599,13 +598,48 @@ class RoomListPresenterTest {
}
}
+ @Test
+ fun `present - notification sound banner`() = runTest {
+ val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> }
+ val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
+ val matrixClient = FakeMatrixClient(
+ roomListService = roomListService,
+ )
+ val roomSummary = aRoomSummary(
+ currentUserMembership = CurrentUserMembership.INVITED
+ )
+ roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
+ roomListService.postAllRooms(listOf(roomSummary))
+ val onAnnouncementDismissedResult = lambdaRecorder { }
+ val announcementService = FakeAnnouncementService(
+ onAnnouncementDismissedResult = onAnnouncementDismissedResult,
+ )
+ val presenter = createRoomListPresenter(
+ client = matrixClient,
+ announcementService = announcementService,
+ )
+ presenter.test {
+ assertThat(announcementService.announcementsToShowFlow().first()).isEmpty()
+ skipItems(1)
+ val state = awaitItem()
+ assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
+ announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
+ assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
+ state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
+ onAnnouncementDismissedResult.assertions().isCalledOnce()
+ .with(value(Announcement.NewNotificationSound))
+ // Simulate service updating the value
+ announcementService.emitAnnouncementsToShow(emptyList())
+ assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isFalse()
+ }
+ }
+
private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
dateFormatter: DateFormatter = FakeDateFormatter(),
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
- featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter = Presenter { aRoomListSearchState() },
@@ -613,6 +647,7 @@ class RoomListPresenterTest {
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
+ announcementService: AnnouncementService = FakeAnnouncementService(),
) = RoomListPresenter(
client = client,
leaveRoomPresenter = { leaveRoomState },
@@ -623,11 +658,10 @@ class RoomListPresenterTest {
roomLastMessageFormatter = roomLastMessageFormatter,
),
coroutineDispatchers = testCoroutineDispatchers(),
- notificationSettingsService = client.notificationSettingsService(),
+ notificationSettingsService = client.notificationSettingsService,
sessionCoroutineScope = backgroundScope,
dateTimeObserver = FakeDateTimeObserver(),
),
- featureFlagService = featureFlagService,
searchPresenter = searchPresenter,
sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter,
@@ -638,5 +672,6 @@ class RoomListPresenterTest {
notificationCleaner = notificationCleaner,
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
+ announcementService = announcementService,
)
}
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
index 3837fe6f55e..2f158ec4b93 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
index 0465926abc5..5c3bef26c4d 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -284,12 +285,13 @@ private fun AndroidComposeTestRule.setRoomL
onSettingsClick = onSettingsClick,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onCreateRoomClick = onCreateRoomClick,
+ onStartChatClick = onCreateRoomClick,
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,
onReportRoomClick = onReportRoomClick,
- acceptDeclineInviteView = { },
+ acceptDeclineInviteView = {},
+ leaveRoomView = {},
)
}
}
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt
index 7649b6141ee..d21f3a09a8a 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt
new file mode 100644
index 00000000000..c7608833ac3
--- /dev/null
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.home.impl.spaces
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.invite.api.SeenInvitesStore
+import io.element.android.features.invite.test.InMemorySeenInvitesStore
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class HomeSpacesPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createPresenter()
+ presenter.test {
+ val state = awaitItem()
+ assertThat(state.space).isEqualTo(CurrentSpace.Root)
+ assertThat(state.spaceRooms).isEmpty()
+ assertThat(state.hideInvitesAvatar).isFalse()
+ assertThat(state.seenSpaceInvites).isEmpty()
+ }
+ }
+
+ private fun createPresenter(
+ client: MatrixClient = FakeMatrixClient(),
+ seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
+ ) = HomeSpacesPresenter(
+ client = client,
+ seenInvitesStore = seenInvitesStore,
+ )
+}
diff --git a/features/invite/api/build.gradle.kts b/features/invite/api/build.gradle.kts
index d17b392e594..084ad1088d4 100644
--- a/features/invite/api/build.gradle.kts
+++ b/features/invite/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt
index eb1dfd3df67..696e02a0d71 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -36,3 +38,11 @@ fun RoomInfo.toInviteData(): InviteData {
isDm = isDm,
)
}
+
+fun SpaceRoom.toInviteData(): InviteData {
+ return InviteData(
+ roomId = roomId,
+ roomName = displayName,
+ isDm = false,
+ )
+}
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt
index 682970ffe7d..046303ca4fd 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt
index 015fc161822..2caaacab2bb 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
index f4645383df6..3bfd0430fea 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
new file mode 100644
index 00000000000..18acc11e5e3
--- /dev/null
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invite.api.acceptdecline
+
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.core.RoomId
+
+fun anAcceptDeclineInviteState(
+ acceptAction: AsyncAction = AsyncAction.Uninitialized,
+ declineAction: AsyncAction = AsyncAction.Uninitialized,
+ eventSink: (AcceptDeclineInviteEvents) -> Unit = {},
+) = AcceptDeclineInviteState(
+ acceptAction = acceptAction,
+ declineAction = declineAction,
+ eventSink = eventSink,
+)
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt
index 78be5ebd165..b6eec03a6f8 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,7 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.core.RoomId
-interface AcceptDeclineInviteView {
+fun interface AcceptDeclineInviteView {
@Composable
fun Render(
state: AcceptDeclineInviteState,
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt
index f0c43663512..0ebf6759665 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
index 27080ee354c..4174d992226 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,6 +13,10 @@ import com.bumble.appyx.core.node.Node
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint
-interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
- fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
+fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inviteData: InviteData,
+ ): Node
}
diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts
index d6337f65c9f..80b98464f75 100644
--- a/features/invite/impl/build.gradle.kts
+++ b/features/invite/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,7 +23,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.invite.api)
@@ -36,17 +38,9 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(projects.libraries.push.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
+ testCommonDependencies(libs, true)
testImplementation(projects.features.invite.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt
index 716cd7f9070..217c5e4c74c 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt
@@ -1,13 +1,14 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.core.extensions.mapFailure
@@ -19,7 +20,6 @@ import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
-import javax.inject.Inject
interface AcceptInvite {
suspend operator fun invoke(roomId: RoomId): Result
@@ -30,7 +30,7 @@ interface AcceptInvite {
}
@ContributesBinding(SessionScope::class)
-class DefaultAcceptInvite @Inject constructor(
+class DefaultAcceptInvite(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt
index 9276f373652..ca3ad283958 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt
@@ -1,19 +1,19 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.invite.api.SeenInvitesStore
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 io.element.android.libraries.push.api.notifications.NotificationCleaner
-import javax.inject.Inject
interface DeclineInvite {
suspend operator fun invoke(
@@ -32,7 +32,7 @@ interface DeclineInvite {
}
@ContributesBinding(SessionScope::class)
-class DefaultDeclineInvite @Inject constructor(
+class DefaultDeclineInvite(
private val client: MatrixClient,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt
index 38295daa27b..9e36b686560 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -33,8 +34,7 @@ class DefaultSeenInvitesStore(
) : SeenInvitesStore {
init {
sessionObserver.addListener(object : SessionListener {
- override suspend fun onSessionCreated(userId: String) = Unit
- override suspend fun onSessionDeleted(userId: String) {
+ override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
if (sessionId.value == userId) {
clear()
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt
index 256214e5d2a..b1a46f8035e 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt
@@ -1,27 +1,27 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl
import android.content.Context
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class DefaultSeenInvitesStoreFactory @Inject constructor(
+class DefaultSeenInvitesStoreFactory(
@ApplicationContext private val context: Context,
private val sessionObserver: SessionObserver,
) : SeenInvitesStoreFactory {
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt
index 4d681e84624..5b5ef54077f 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
index 6dd75761d31..9be255fe1a6 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,6 +13,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@@ -24,9 +26,9 @@ import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class AcceptDeclineInvitePresenter @Inject constructor(
+@Inject
+class AcceptDeclineInvitePresenter(
private val acceptInvite: AcceptInvite,
private val declineInvite: DeclineInvite,
) : Presenter {
@@ -38,7 +40,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
val declinedAction: MutableState> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
- fun handleEvents(event: AcceptDeclineInviteEvents) {
+ fun handleEvent(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
@@ -56,15 +58,11 @@ class AcceptDeclineInvitePresenter @Inject constructor(
)
}
}
- is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
- declinedAction.value = AsyncAction.Uninitialized
- }
-
- is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
+ is InternalAcceptDeclineInviteEvents.ClearAcceptActionState -> {
acceptedAction.value = AsyncAction.Uninitialized
}
- is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
+ is InternalAcceptDeclineInviteEvents.ClearDeclineActionState -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
@@ -73,7 +71,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
return AcceptDeclineInviteState(
acceptAction = acceptedAction.value,
declineAction = declinedAction.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt
index 9896de1d3e9..3f8bf93afaa 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,9 +10,9 @@ package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.InviteData
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
+import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
@@ -51,13 +52,3 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized,
- declineAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
-) = AcceptDeclineInviteState(
- acceptAction = acceptAction,
- declineAction = declineAction,
- eventSink = eventSink,
-)
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
index 9c672594e06..da035f0ff18 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -35,9 +36,12 @@ fun AcceptDeclineInviteView(
Box(modifier = modifier) {
AsyncActionView(
async = state.acceptAction,
- onSuccess = onAcceptInviteSuccess,
+ onSuccess = { roomId ->
+ state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
+ onAcceptInviteSuccess(roomId)
+ },
onErrorDismiss = {
- state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
+ state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -52,9 +56,12 @@ fun AcceptDeclineInviteView(
)
AsyncActionView(
async = state.declineAction,
- onSuccess = onDeclineInviteSuccess,
+ onSuccess = { roomId ->
+ state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
+ onDeclineInviteSuccess(roomId)
+ },
onErrorDismiss = {
- state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
+ state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -78,7 +85,7 @@ fun AcceptDeclineInviteView(
)
},
onDismissClick = {
- state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
+ state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
}
)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt
index e37582f63f5..f819de650c8 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,15 +10,14 @@ package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
-import javax.inject.Inject
@ContributesBinding(SessionScope::class)
-class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView {
+class DefaultAcceptDeclineInviteView : AcceptDeclineInviteView {
@Composable
override fun Render(
state: AcceptDeclineInviteState,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt
index 44234304322..765c7055d36 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +11,6 @@ package io.element.android.features.invite.impl.acceptdecline
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
- data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
- data object DismissAcceptError : InternalAcceptDeclineInviteEvents
- data object DismissDeclineError : InternalAcceptDeclineInviteEvents
+ data object ClearAcceptActionState : InternalAcceptDeclineInviteEvents
+ data object ClearDeclineActionState : InternalAcceptDeclineInviteEvents
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt
index 033d2e58def..1fc160e5305 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
index 100a5de4b86..2fe5c230885 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,16 +13,17 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class DeclineAndBlockNode @AssistedInject constructor(
+@AssistedInject
+class DeclineAndBlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
presenterFactory: DeclineAndBlockPresenter.Factory,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
index 9a18aa746bd..af2a10cbfdf 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,9 +16,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
@@ -28,13 +29,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-class DeclineAndBlockPresenter @AssistedInject constructor(
+@AssistedInject
+class DeclineAndBlockPresenter(
@Assisted private val inviteData: InviteData,
private val declineInvite: DeclineInvite,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter {
@AssistedFactory
- interface Factory {
+ fun interface Factory {
fun create(inviteData: InviteData): DeclineAndBlockPresenter
}
@@ -47,7 +49,7 @@ class DeclineAndBlockPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope()
- fun handleEvents(event: DeclineAndBlockEvents) {
+ fun handleEvent(event: DeclineAndBlockEvents) {
when (event) {
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
@@ -62,7 +64,7 @@ class DeclineAndBlockPresenter @AssistedInject constructor(
reportReason = reportReason,
blockUser = blockUser,
declineAction = declineAction.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
index 0ee7444b2ce..7dbbbfd60e1 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt
index 87016d13cca..2e782143060 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
index 05110f982c1..1028a4c1423 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
index 5eab22dd912..d6e93fb8051 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,16 +10,19 @@ package io.element.android.features.invite.impl.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint {
- override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node {
+class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inviteData: InviteData,
+ ): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode(buildContext, plugins = listOf(inputs))
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
index 4f5da603a6a..f78c0358a6a 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
@@ -1,16 +1,17 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.Provides
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
@@ -20,7 +21,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
@ContributesTo(SessionScope::class)
-@Module
+@BindingContainer
interface InviteModule {
@Binds
fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter
diff --git a/features/invite/impl/src/main/res/values-be/translations.xml b/features/invite/impl/src/main/res/values-be/translations.xml
index 13c241102fa..fca38b796a1 100644
--- a/features/invite/impl/src/main/res/values-be/translations.xml
+++ b/features/invite/impl/src/main/res/values-be/translations.xml
@@ -1,5 +1,6 @@
+ "Заблакіраваць карыстальніка""Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?""Адхіліць запрашэнне""Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"
diff --git a/features/invite/impl/src/main/res/values-bg/translations.xml b/features/invite/impl/src/main/res/values-bg/translations.xml
index 5e3c9a6fbdf..d4c19eca2bd 100644
--- a/features/invite/impl/src/main/res/values-bg/translations.xml
+++ b/features/invite/impl/src/main/res/values-bg/translations.xml
@@ -1,8 +1,12 @@
"Блокиране на потребителя"
+ "Отхвърляне и блокиране""Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?""Отказване на покана"
+ "Сигурни ли сте, че искате да откажете този личен чат с %1$s?"
+ "Отказване на чат""Няма покани""%1$s (%2$s) ви покани"
+ "Отхвърляне и блокиране"
diff --git a/features/invite/impl/src/main/res/values-cs/translations.xml b/features/invite/impl/src/main/res/values-cs/translations.xml
index f5762693435..28da3dc4b54 100644
--- a/features/invite/impl/src/main/res/values-cs/translations.xml
+++ b/features/invite/impl/src/main/res/values-cs/translations.xml
@@ -3,7 +3,7 @@
"Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti""Zablokovat uživatele""Nahlaste tuto místnost svému poskytovateli účtu."
- "Popište důvod nahlášení…"
+ "Popište důvod…""Odmítnout a zablokovat""Opravdu chcete odmítnout pozvánku do %1$s?""Odmítnout pozvání"
diff --git a/features/invite/impl/src/main/res/values-cy/translations.xml b/features/invite/impl/src/main/res/values-cy/translations.xml
index a84fa2397a9..e32337a162f 100644
--- a/features/invite/impl/src/main/res/values-cy/translations.xml
+++ b/features/invite/impl/src/main/res/values-cy/translations.xml
@@ -3,7 +3,7 @@
"Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn""Rhwystro defnyddiwr""Adrodd am yr ystafell hon i ddarparwr eich cyfrif."
- "Disgrifiwch y rheswm dros adrodd…"
+ "Disgrifiwch y rheswm…""Gwrthod a rhwystro""Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?""Gwrthod y gwahoddiad"
diff --git a/features/invite/impl/src/main/res/values-de/translations.xml b/features/invite/impl/src/main/res/values-de/translations.xml
index e3937fbda13..7b6ca2eb611 100644
--- a/features/invite/impl/src/main/res/values-de/translations.xml
+++ b/features/invite/impl/src/main/res/values-de/translations.xml
@@ -1,18 +1,18 @@
- "Sie werden keine Nachrichten oder Chateinladungen von diesem Nutzer sehen."
- "Benutzer blockieren"
- "Melden Sie diesen Raum Ihrem Kontoanbieter."
- "Nennen Sie den Grund für die Meldung…"
+ "Du wirst keine Nachrichten oder Chat-Einladungen von diesem Nutzer sehen."
+ "Nutzer blockieren"
+ "Melde diesen Chat deinem Konto-Anbieter."
+ "Beschreibe den Grund für die Meldung…""Ablehnen und blockieren"
- "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"
+ "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?""Einladung ablehnen"
- "Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?"
+ "Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?""Einladung ablehnen""Keine Einladungen""%1$s (%2$s) hat dich eingeladen""Ja, ablehnen & blockieren"
- "Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."
+ "Bist du sicher, dass du die Einladung zu diesem Chat ablehnen möchtest? Dadurch wird auch jede weitere Kontaktaufnahme oder Chat Einladung von %1$s blockiert.""Einladung ablehnen & Nutzer blockieren""Ablehnen und blockieren"
diff --git a/features/invite/impl/src/main/res/values-et/translations.xml b/features/invite/impl/src/main/res/values-et/translations.xml
index 91906fa8778..1bc1e93b595 100644
--- a/features/invite/impl/src/main/res/values-et/translations.xml
+++ b/features/invite/impl/src/main/res/values-et/translations.xml
@@ -3,7 +3,7 @@
"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid""Blokeeri kasutaja""Teata sellest jututoast oma teenusepakkujale."
- "Kirjelda teatamise põhjust…"
+ "Kirjelda põhjust…""Keeldu ja blokeeri""Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?""Lükka kutse tagasi"
diff --git a/features/invite/impl/src/main/res/values-eu/translations.xml b/features/invite/impl/src/main/res/values-eu/translations.xml
index d60aeced771..dfc83103667 100644
--- a/features/invite/impl/src/main/res/values-eu/translations.xml
+++ b/features/invite/impl/src/main/res/values-eu/translations.xml
@@ -1,9 +1,13 @@
+ "Blokeatu erabiltzailea"
+ "Baztertu eta blokeatu""Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?""Baztertu gonbidapena""Ziur %1$s(r)en txat pribatua baztertu nahi duzula?""Baztertu txata""Ez dago gonbidapenik""%1$s(e)k (%2$s) gonbidatu zaitu"
+ "Eman gonbidapenari ezetza eta blokeatu"
+ "Baztertu eta blokeatu"
diff --git a/features/invite/impl/src/main/res/values-fi/translations.xml b/features/invite/impl/src/main/res/values-fi/translations.xml
index 47ad368d738..215e2ee31b4 100644
--- a/features/invite/impl/src/main/res/values-fi/translations.xml
+++ b/features/invite/impl/src/main/res/values-fi/translations.xml
@@ -3,7 +3,7 @@
"Et tule näkemään viestejä tai kutsuja tältä käyttäjältä""Estä käyttäjä""Ilmoita tästä huoneesta palveluntarjoajallesi."
- "Kerro syy ilmoittamiseen…"
+ "Kuvaile syytä…""Hylkää ja estä""Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?""Hylkää kutsu"
diff --git a/features/invite/impl/src/main/res/values-hu/translations.xml b/features/invite/impl/src/main/res/values-hu/translations.xml
index 97595ed421d..e75d2961ed7 100644
--- a/features/invite/impl/src/main/res/values-hu/translations.xml
+++ b/features/invite/impl/src/main/res/values-hu/translations.xml
@@ -3,8 +3,8 @@
"Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni.""Felhasználó letiltása""A szoba jelentése a fiókszolgáltatójának."
- "Írja le a jelentés okát…"
- "Elutasítás és blokkolás"
+ "Írja le az okot…"
+ "Elutasítás és letiltás""Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?""Meghívás elutasítása""Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"
@@ -14,5 +14,5 @@
"Igen, elutasítás és blokkolás""Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja.""Meghívó elutasítása és blokkolás"
- "Elutasítás és blokkolás"
+ "Elutasítás és letiltás"
diff --git a/features/invite/impl/src/main/res/values-ka/translations.xml b/features/invite/impl/src/main/res/values-ka/translations.xml
index 3accaf9eda6..7b60e8f7ee4 100644
--- a/features/invite/impl/src/main/res/values-ka/translations.xml
+++ b/features/invite/impl/src/main/res/values-ka/translations.xml
@@ -1,5 +1,6 @@
+ "მომხმარებლის დაბლოკვა""დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?""მოწვევაზე უარის თქმა""დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"
diff --git a/features/invite/impl/src/main/res/values-ko/translations.xml b/features/invite/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..169d5ab668c
--- /dev/null
+++ b/features/invite/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "이 사용자로부터 메시지나 방 초대장이 표시되지 않습니다."
+ "사용자 차단하기"
+ "이 room 계정 제공자에게 신고하세요."
+ "신고 사유를 설명하세요…"
+ "거부 및 차단"
+ "정말로 %1$s 에 참가하지 않고 초대를 거절하시겠어요?"
+ "초대 거절"
+ "%1$s 와의 비공개 채팅을 정말 거부하시겠습니까?"
+ "채팅 거절"
+ "초대 없음"
+ "%1$s (%2$s) 당신을 초대했습니다"
+ "예, 거부 및 차단"
+ "이 방에 대한 초대 거부를 정말로 확인하시겠습니까? 이 경우 %1$s 에서 귀하에게 연락하거나 방에 초대할 수 없게 됩니다."
+ "초대 거부 및 차단"
+ "거부 및 차단"
+
diff --git a/features/invite/impl/src/main/res/values-lt/translations.xml b/features/invite/impl/src/main/res/values-lt/translations.xml
index c94b61fd5d5..375aab7f359 100644
--- a/features/invite/impl/src/main/res/values-lt/translations.xml
+++ b/features/invite/impl/src/main/res/values-lt/translations.xml
@@ -1,5 +1,6 @@
+ "Blokuoti vartotoją""Ar tikrai norite atmesti kvietimą prisijungti prie %1$s?""Atmesti kvietimą""Ar tikrai norite atmesti šį privatų pokalbį su %1$s ?"
diff --git a/features/invite/impl/src/main/res/values-nb/translations.xml b/features/invite/impl/src/main/res/values-nb/translations.xml
index 25234663487..31091a1d929 100644
--- a/features/invite/impl/src/main/res/values-nb/translations.xml
+++ b/features/invite/impl/src/main/res/values-nb/translations.xml
@@ -3,7 +3,7 @@
"Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren""Blokker bruker""Rapporter dette rommet til din kontoleverandør."
- "Beskriv årsaken for å rapportere…"
+ "Beskriv årsaken…""Avslå og blokker""Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?""Avvis invitasjon"
diff --git a/features/invite/impl/src/main/res/values-nl/translations.xml b/features/invite/impl/src/main/res/values-nl/translations.xml
index 25eee1572d1..de0f69097ca 100644
--- a/features/invite/impl/src/main/res/values-nl/translations.xml
+++ b/features/invite/impl/src/main/res/values-nl/translations.xml
@@ -1,9 +1,12 @@
+ "Gebruiker blokkeren"
+ "Weigeren en blokkeren""Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?""Uitnodiging weigeren""Weet je zeker dat je deze privéchat met %1$s wilt weigeren?""Chat weigeren""Geen uitnodigingen""%1$s (%2$s) heeft je uitgenodigd"
+ "Weigeren en blokkeren"
diff --git a/features/invite/impl/src/main/res/values-pl/translations.xml b/features/invite/impl/src/main/res/values-pl/translations.xml
index e8636fc3dc1..b20ee321ea2 100644
--- a/features/invite/impl/src/main/res/values-pl/translations.xml
+++ b/features/invite/impl/src/main/res/values-pl/translations.xml
@@ -3,7 +3,7 @@
"Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika""Zablokuj użytkownika""Zgłoś pokój dostawcy swojego konta."
- "Opisz powód zgłoszenia…"
+ "Opisz powód…""Odrzuć i zablokuj""Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?""Odrzuć zaproszenie"
diff --git a/features/invite/impl/src/main/res/values-pt-rBR/translations.xml b/features/invite/impl/src/main/res/values-pt-rBR/translations.xml
index 43569cdd312..bf0ee63061b 100644
--- a/features/invite/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/invite/impl/src/main/res/values-pt-rBR/translations.xml
@@ -3,16 +3,16 @@
"Você não verá nenhuma mensagem ou convite de sala deste usuário""Bloquear usuário""Denuncie esta sala ao fornecedor da sua conta."
- "Descreva o motivo da denúncia…"
+ "Descreva o motivo para denunciar…""Recusar e bloquear"
- "Tem certeza de que deseja recusar o convite para ingressar em %1$s?"
+ "Tem certeza de que deseja recusar o convite para entrar em %1$s?""Recusar convite"
- "Tem certeza de que deseja recusar esse chat privado com %1$s?"
+ "Tem certeza de que deseja recusar esse conversa privada com %1$s?""Recusar chat"
- "Sem convites"
+ "Não há convites""%1$s(%2$s) convidou você""Sim, recusar e bloquear"
- "Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."
+ "Tem certeza de que quer recusar o convite para entrar nesta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para salas.""Recusar convite e bloquear""Recusar e bloquear"
diff --git a/features/invite/impl/src/main/res/values-pt/translations.xml b/features/invite/impl/src/main/res/values-pt/translations.xml
index 5813c9e54f5..f00ce6151ba 100644
--- a/features/invite/impl/src/main/res/values-pt/translations.xml
+++ b/features/invite/impl/src/main/res/values-pt/translations.xml
@@ -1,13 +1,13 @@
- "Não verás quaisquer mensagens ou convites deste utilizador"
+ "Não vais ver quaisquer mensagens ou convites para sala deste utilizador""Bloquear utilizador"
- "Denunciar esta sala ao teu operador de conta."
- "Descreve a razão de denúncia…"
- "Rejeitar e bloquear"
+ "Denunciar esta sala ao fornecedor da tua conta."
+ "Descreve a razão para denunciar…"
+ "Recusar e bloquear""Tens a certeza que queres rejeitar o convite para entra em %1$s?""Rejeitar convite"
- "Tem a certeza que queres rejeitar esta conversa privada com %1$s?"
+ "Tens a certeza que queres rejeitar esta conversa privada com %1$s?""Rejeitar conversa""Sem convites""%1$s (%2$s) convidou-te"
diff --git a/features/invite/impl/src/main/res/values-ro/translations.xml b/features/invite/impl/src/main/res/values-ro/translations.xml
index 3f00d323377..747efbed6d4 100644
--- a/features/invite/impl/src/main/res/values-ro/translations.xml
+++ b/features/invite/impl/src/main/res/values-ro/translations.xml
@@ -1,9 +1,18 @@
+ "Nu veți vedea niciun mesaj sau invitație de la acest utilizator."
+ "Blocați utilizatorul"
+ "Raportați această cameră furnizorului contului dumneavoastră"
+ "Descrieți motivul raportării…"
+ "Refuzați și blocați""Sigur doriți să refuzați alăturarea la %1$s?""Refuzați invitația""Sigur doriți să refuzați conversațiile cu %1$s?""Refuzați conversația""Nicio invitație""%1$s (%2$s) v-a invitat."
+ "Da, refuzați și blocați"
+ "Sunteți sigur că doriți să refuzați invitația de a vă alătura acestei camere? Acest lucru va împiedica, de asemenea, %1$s să vă contacteze sau să vă invite în camere."
+ "Refuzați invitația și blocați"
+ "Refuzați și blocați"
diff --git a/features/invite/impl/src/main/res/values-sk/translations.xml b/features/invite/impl/src/main/res/values-sk/translations.xml
index a14b5b0dae5..826aa023f65 100644
--- a/features/invite/impl/src/main/res/values-sk/translations.xml
+++ b/features/invite/impl/src/main/res/values-sk/translations.xml
@@ -3,7 +3,7 @@
"Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti""Zablokovať používateľa""Nahlásiť túto miestnosť poskytovateľovi účtu."
- "Opíšte dôvod nahlásenia…"
+ "Popíšte dôvod…""Odmietnuť a zablokovať""Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?""Odmietnuť pozvanie"
diff --git a/features/invite/impl/src/main/res/values-sv/translations.xml b/features/invite/impl/src/main/res/values-sv/translations.xml
index a57336b50e4..f44783189a7 100644
--- a/features/invite/impl/src/main/res/values-sv/translations.xml
+++ b/features/invite/impl/src/main/res/values-sv/translations.xml
@@ -3,7 +3,7 @@
"Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren""Blockera användare""Rapportera det här rummet till din kontoleverantör."
- "Beskriv skälet för anmälan …"
+ "Beskriv anledningen …""Avvisa och blockera""Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?""Avböj inbjudan"
diff --git a/features/invite/impl/src/main/res/values-tr/translations.xml b/features/invite/impl/src/main/res/values-tr/translations.xml
index 7c60a22b8a5..26ed1cb6d86 100644
--- a/features/invite/impl/src/main/res/values-tr/translations.xml
+++ b/features/invite/impl/src/main/res/values-tr/translations.xml
@@ -1,5 +1,6 @@
+ "Kullanıcıyı engelle""%1$s katılma davetini reddetmek istediğinizden emin misiniz?""Daveti reddet""%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?"
@@ -9,5 +10,4 @@
"Evet, reddet ve engelle""Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller.""Daveti reddet ve engelle"
- "Reddet ve engelle"
diff --git a/features/invite/impl/src/main/res/values-uk/translations.xml b/features/invite/impl/src/main/res/values-uk/translations.xml
index 9a5aef72987..8752f5707ed 100644
--- a/features/invite/impl/src/main/res/values-uk/translations.xml
+++ b/features/invite/impl/src/main/res/values-uk/translations.xml
@@ -3,7 +3,7 @@
"Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача""Заблокувати користувача""Поскаржитися на цю кімнату постачальнику облікового запису."
- "Опишіть причину скарги…"
+ "Опишіть причину…""Відхилити та заблокувати""Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?""Відхилити запрошення"
diff --git a/features/invite/impl/src/main/res/values-ur/translations.xml b/features/invite/impl/src/main/res/values-ur/translations.xml
index 641f5e7b7e7..42bb36ee14e 100644
--- a/features/invite/impl/src/main/res/values-ur/translations.xml
+++ b/features/invite/impl/src/main/res/values-ur/translations.xml
@@ -1,5 +1,6 @@
+ "صارف کو مسدود کریں""کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟""دعوت مسترد کریں""کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟"
diff --git a/features/invite/impl/src/main/res/values-uz/translations.xml b/features/invite/impl/src/main/res/values-uz/translations.xml
index 78e3975700d..bdefbd31d33 100644
--- a/features/invite/impl/src/main/res/values-uz/translations.xml
+++ b/features/invite/impl/src/main/res/values-uz/translations.xml
@@ -1,9 +1,12 @@
+ "Foydalanuvchini bloklash"
+ "Rad etish va bloklash""Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?""Taklifni rad etish""Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?""Chatni rad etish""Takliflar yo\'q""%1$s(%2$s ) sizni taklif qildi"
+ "Rad etish va bloklash"
diff --git a/features/invite/impl/src/main/res/values-zh/translations.xml b/features/invite/impl/src/main/res/values-zh/translations.xml
index 3d254344382..e7cf39a9c32 100644
--- a/features/invite/impl/src/main/res/values-zh/translations.xml
+++ b/features/invite/impl/src/main/res/values-zh/translations.xml
@@ -1,9 +1,18 @@
+ "您不会看到来自该用户的任何信息或房间邀请"
+ "封禁用户"
+ "向您的帐户提供商举报此房间。"
+ "描述举报的原因…"
+ "拒绝并屏蔽""您确定要拒绝加入 %1$s 的邀请吗?""拒绝邀请""您确定要拒绝与 %1$s 开始私聊吗?""拒绝聊天""没有邀请""%1$s (%2$s)邀请了你"
+ "是的,拒绝并屏蔽"
+ "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"
+ "拒绝邀请并屏蔽"
+ "拒绝并屏蔽"
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt
index eeefe688a17..b1a0c30a380 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt
index cb07bd5191f..40621f4c082 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -34,14 +35,14 @@ class DefaultDeclineInviteTest {
private val notificationCleaner =
FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda)
- private val successLeaveRoomLambda = lambdaRecorder> { -> Result.success(Unit) }
+ private val successLeaveRoomLambda = lambdaRecorder> { Result.success(Unit) }
private val successIgnoreUserLambda =
lambdaRecorder> { _ -> Result.success(Unit) }
private val successReportRoomLambda =
lambdaRecorder> { _ -> Result.success(Unit) }
private val failureLeaveRoomLambda =
- lambdaRecorder> { -> Result.failure(Exception("Leave room error")) }
+ lambdaRecorder> { Result.failure(Exception("Leave room error")) }
private val failureIgnoreUserLambda =
lambdaRecorder> { _ -> Result.failure(Exception("Ignore user error")) }
private val failureReportRoomLambda =
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
index c8a427bea6a..ba080293f8e 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -56,7 +57,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
- InternalAcceptDeclineInviteEvents.CancelDeclineInvite
+ InternalAcceptDeclineInviteEvents.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -90,7 +91,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
- InternalAcceptDeclineInviteEvents.DismissDeclineError
+ InternalAcceptDeclineInviteEvents.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -154,7 +155,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
- InternalAcceptDeclineInviteEvents.DismissAcceptError
+ InternalAcceptDeclineInviteEvents.ClearAcceptActionState
)
}
awaitItem().also { state ->
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
index c5d2f53c92c..67c2d55490c 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,11 +12,11 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
+import io.element.android.features.invite.test.anInviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
-import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -148,28 +149,16 @@ class DeclineAndBlockPresenterTest {
.isCalledOnce()
.with(value(A_ROOM_ID), value(true), value(false), value(""))
}
+}
- private fun anInviteData(
- roomId: RoomId = A_ROOM_ID,
- name: String = A_ROOM_NAME,
- isDm: Boolean = false,
- ): InviteData {
- return InviteData(
- roomId = roomId,
- roomName = name,
- isDm = isDm,
- )
- }
-
- private fun createDeclineAndBlockPresenter(
- inviteData: InviteData = anInviteData(),
- declineInvite: DeclineInvite = FakeDeclineInvite(),
- snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
- ): DeclineAndBlockPresenter {
- return DeclineAndBlockPresenter(
- inviteData = inviteData,
- declineInvite = declineInvite,
- snackbarDispatcher = snackbarDispatcher,
- )
- }
+internal fun createDeclineAndBlockPresenter(
+ inviteData: InviteData = anInviteData(),
+ declineInvite: DeclineInvite = FakeDeclineInvite(),
+ snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
+): DeclineAndBlockPresenter {
+ return DeclineAndBlockPresenter(
+ inviteData = inviteData,
+ declineInvite = declineInvite,
+ snackbarDispatcher = snackbarDispatcher,
+ )
}
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
index 60e00f8e35b..299fec85652 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt
new file mode 100644
index 00000000000..13bb13dccf7
--- /dev/null
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invite.impl.declineandblock
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.invite.test.anInviteData
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultDeclineAndBlockEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultDeclineAndBlockEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ DeclineAndBlockNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenterFactory = { inviteData -> createDeclineAndBlockPresenter() }
+ )
+ }
+ val inviteData = anInviteData()
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ inviteData = inviteData
+ )
+ assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java)
+ assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData))
+ }
+}
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt
index 1ba95bc76ed..de210de5589 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt
index 18b46dbd5f2..cb7af7828b3 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts
index e504873c3b8..2df267f1558 100644
--- a/features/invite/test/build.gradle.kts
+++ b/features/invite/test/build.gradle.kts
@@ -1,17 +1,9 @@
/*
- * Copyright (c) 2025 New Vector Ltd
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 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.
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
*/
plugins {
@@ -26,5 +18,6 @@ dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.test)
+ implementation(projects.tests.testutils)
api(projects.features.invite.api)
}
diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt
index 25db72532e7..709a4a0f2a1 100644
--- a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt
+++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt
index 62e6d8b3222..84810f2e651 100644
--- a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt
+++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt
new file mode 100644
index 00000000000..52d4d177901
--- /dev/null
+++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invite.test.declineandblock
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.invite.api.InviteData
+import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inviteData: InviteData,
+ ): Node {
+ lambdaError()
+ }
+}
diff --git a/features/invitepeople/api/build.gradle.kts b/features/invitepeople/api/build.gradle.kts
new file mode 100644
index 00000000000..9fba48462ec
--- /dev/null
+++ b/features/invitepeople/api/build.gradle.kts
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.invitepeople.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
new file mode 100644
index 00000000000..264aafd5708
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+interface InvitePeopleEvents {
+ data object SendInvites : InvitePeopleEvents
+ data object CloseSearch : InvitePeopleEvents
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt
new file mode 100644
index 00000000000..0be07986809
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+
+interface InvitePeoplePresenter : Presenter {
+ interface Factory {
+ fun create(
+ joinedRoom: JoinedRoom?,
+ roomId: RoomId,
+ ): InvitePeoplePresenter
+ }
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt
new file mode 100644
index 00000000000..7bad81ab658
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+interface InvitePeopleRenderer {
+ @Composable
+ fun Render(
+ state: InvitePeopleState,
+ modifier: Modifier,
+ )
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
new file mode 100644
index 00000000000..9d342d191fb
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+import io.element.android.libraries.architecture.AsyncAction
+
+interface InvitePeopleState {
+ val canInvite: Boolean
+ val isSearchActive: Boolean
+ val sendInvitesAction: AsyncAction
+ val eventSink: (InvitePeopleEvents) -> Unit
+}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
new file mode 100644
index 00000000000..ce30bcc1f6d
--- /dev/null
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.api
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncAction
+
+class InvitePeopleStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aPreviewInvitePeopleState(),
+ aPreviewInvitePeopleState(canInvite = true),
+ aPreviewInvitePeopleState(isSearchActive = true),
+ aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
+ )
+}
+
+private data class PreviewInvitePeopleState(
+ override val canInvite: Boolean,
+ override val isSearchActive: Boolean,
+ override val sendInvitesAction: AsyncAction,
+ override val eventSink: (InvitePeopleEvents) -> Unit,
+) : InvitePeopleState
+
+private fun aPreviewInvitePeopleState(
+ canInvite: Boolean = false,
+ isSearchActive: Boolean = false,
+ sendInvitesAction: AsyncAction = AsyncAction.Uninitialized,
+ eventSink: (InvitePeopleEvents) -> Unit = {},
+) = PreviewInvitePeopleState(
+ canInvite = canInvite,
+ isSearchActive = isSearchActive,
+ sendInvitesAction = sendInvitesAction,
+ eventSink = eventSink
+)
diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts
new file mode 100644
index 00000000000..e2025405ff3
--- /dev/null
+++ b/features/invitepeople/impl/build.gradle.kts
@@ -0,0 +1,46 @@
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
+
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.invitepeople.impl"
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupDependencyInjection()
+
+dependencies {
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.androidutils)
+ implementation(projects.libraries.usersearch.impl)
+ implementation(libs.coil.compose)
+ implementation(projects.services.apperror.api)
+ api(projects.features.invitepeople.api)
+
+ testCommonDependencies(libs, true)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.usersearch.test)
+ testImplementation(projects.services.apperror.test)
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
new file mode 100644
index 00000000000..b0c8994a62d
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
+ data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
+ data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
+ data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
new file mode 100644
index 00000000000..5ac3bc28243
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeoplePresenter
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.map
+import io.element.android.libraries.architecture.runCatchingUpdatingState
+import io.element.android.libraries.architecture.runUpdatingState
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.di.annotations.SessionCoroutineScope
+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.JoinedRoom
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.room.filterMembers
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.services.apperror.api.AppErrorStateService
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@AssistedInject
+class DefaultInvitePeoplePresenter(
+ @Assisted private val joinedRoom: JoinedRoom?,
+ @Assisted private val roomId: RoomId,
+ private val userRepository: UserRepository,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
+ private val appErrorStateService: AppErrorStateService,
+ private val matrixClient: MatrixClient,
+) : InvitePeoplePresenter {
+ @AssistedFactory
+ @ContributesBinding(SessionScope::class)
+ interface Factory : InvitePeoplePresenter.Factory {
+ override fun create(joinedRoom: JoinedRoom?, roomId: RoomId): DefaultInvitePeoplePresenter
+ }
+
+ @Composable
+ override fun present(): InvitePeopleState {
+ val roomMembers = remember { mutableStateOf>>(AsyncData.Loading()) }
+ val selectedUsers = remember { mutableStateOf>(persistentListOf()) }
+ val searchResults = remember { mutableStateOf>>(SearchBarResultState.Initial()) }
+ var searchQuery by rememberSaveable { mutableStateOf("") }
+ var searchActive by rememberSaveable { mutableStateOf(false) }
+ val showSearchLoader = rememberSaveable { mutableStateOf(false) }
+ val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
+
+ val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
+ if (joinedRoom == null) {
+ val result = matrixClient.getJoinedRoom(roomId)
+ value = if (result == null) {
+ AsyncData.Failure(Exception("Room not found"))
+ } else {
+ AsyncData.Success(result)
+ }
+ }
+ }
+
+ LaunchedEffect(room.isSuccess()) {
+ room.dataOrNull()?.let {
+ fetchMembers(it, roomMembers)
+ }
+ }
+ LaunchedEffect(searchQuery, roomMembers) {
+ performSearch(
+ searchResults = searchResults,
+ roomMembers = roomMembers,
+ selectedUsers = selectedUsers,
+ showSearchLoader = showSearchLoader,
+ searchQuery = searchQuery
+ )
+ }
+
+ fun handleEvent(event: InvitePeopleEvents) {
+ when (event) {
+ is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
+ searchActive = event.active
+ searchQuery = ""
+ }
+
+ is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
+ searchQuery = event.query
+ }
+
+ is DefaultInvitePeopleEvents.ToggleUser -> {
+ selectedUsers.toggleUser(event.user)
+ searchResults.toggleUser(event.user)
+ }
+ is InvitePeopleEvents.SendInvites -> {
+ room.dataOrNull()?.let {
+ sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
+ }
+ }
+ is InvitePeopleEvents.CloseSearch -> {
+ searchActive = false
+ searchQuery = ""
+ }
+ }
+ }
+
+ return DefaultInvitePeopleState(
+ room = room.map { },
+ canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
+ selectedUsers = selectedUsers.value,
+ searchQuery = searchQuery,
+ isSearchActive = searchActive,
+ searchResults = searchResults.value,
+ showSearchLoader = showSearchLoader.value,
+ sendInvitesAction = sendInvitesAction.value,
+ eventSink = ::handleEvent,
+ )
+ }
+
+ private fun CoroutineScope.sendInvites(
+ room: JoinedRoom,
+ selectedUsers: List,
+ sendInvitesAction: MutableState>,
+ ) = launch {
+ sendInvitesAction.runUpdatingState {
+ val anyInviteFailed = selectedUsers
+ .map { room.inviteUserById(it.userId) }
+ .any { it.isFailure }
+
+ if (anyInviteFailed) {
+ appErrorStateService.showError(
+ titleRes = CommonStrings.common_unable_to_invite_title,
+ bodyRes = CommonStrings.common_unable_to_invite_message,
+ )
+ }
+
+ Result.success(Unit)
+ }
+ }
+
+ @JvmName("toggleUserInSelectedUsers")
+ private fun MutableState>.toggleUser(user: MatrixUser) {
+ value = if (value.contains(user)) {
+ value.filterNot { it.userId == user.userId }
+ } else {
+ value + user
+ }.toImmutableList()
+ }
+
+ @JvmName("toggleUserInSearchResults")
+ private fun MutableState>>.toggleUser(user: MatrixUser) {
+ val existingResults = value
+ if (existingResults is SearchBarResultState.Results) {
+ value = SearchBarResultState.Results(
+ existingResults.results.map { iu ->
+ if (iu.matrixUser == user) {
+ iu.copy(isSelected = !iu.isSelected)
+ } else {
+ iu
+ }
+ }.toImmutableList()
+ )
+ }
+ }
+
+ private suspend fun performSearch(
+ searchResults: MutableState>>,
+ roomMembers: MutableState>>,
+ selectedUsers: MutableState>,
+ showSearchLoader: MutableState,
+ searchQuery: String,
+ ) = withContext(coroutineDispatchers.io) {
+ searchResults.value = SearchBarResultState.Initial()
+ showSearchLoader.value = false
+ val joinedMembers = roomMembers.value.dataOrNull().orEmpty()
+
+ userRepository.search(searchQuery).onEach { state ->
+ showSearchLoader.value = state.isSearching
+ searchResults.value = when {
+ state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
+ state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
+ else -> SearchBarResultState.Results(state.results.map { result ->
+ val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
+ val isJoined = existingMembership == RoomMembershipState.JOIN
+ val isInvited = existingMembership == RoomMembershipState.INVITE
+ InvitableUser(
+ matrixUser = result.matrixUser,
+ isSelected = selectedUsers.value.contains(result.matrixUser),
+ isAlreadyJoined = isJoined,
+ isAlreadyInvited = isInvited,
+ isUnresolved = result.isUnresolved,
+ )
+ }.toImmutableList())
+ }
+ }.launchIn(this)
+ }
+
+ private suspend fun fetchMembers(
+ room: JoinedRoom,
+ roomMembers: MutableState>>
+ ) {
+ suspend {
+ room.filterMembers("", coroutineDispatchers.io).toImmutableList()
+ }.runCatchingUpdatingState(roomMembers)
+ }
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt
new file mode 100644
index 00000000000..59f25f82f11
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleRenderer.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.invitepeople.api.InvitePeopleRenderer
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.libraries.di.SessionScope
+
+@ContributesBinding(SessionScope::class)
+class DefaultInvitePeopleRenderer : InvitePeopleRenderer {
+ @Composable
+ override fun Render(state: InvitePeopleState, modifier: Modifier) {
+ if (state is DefaultInvitePeopleState) {
+ InvitePeopleView(
+ state = state,
+ modifier = modifier
+ )
+ } else {
+ error("Unsupported state type: ${state::javaClass}")
+ }
+ }
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
new file mode 100644
index 00000000000..917915e4d6e
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import kotlinx.collections.immutable.ImmutableList
+
+data class DefaultInvitePeopleState(
+ val room: AsyncData,
+ override val canInvite: Boolean,
+ val searchQuery: String,
+ val showSearchLoader: Boolean,
+ val searchResults: SearchBarResultState>,
+ val selectedUsers: ImmutableList,
+ override val isSearchActive: Boolean,
+ override val sendInvitesAction: AsyncAction,
+ override val eventSink: (InvitePeopleEvents) -> Unit
+) : InvitePeopleState
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
new file mode 100644
index 00000000000..2f28db87866
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aDefaultInvitePeopleState(),
+ aDefaultInvitePeopleState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query"),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
+ aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
+ aDefaultInvitePeopleState(
+ isSearchActive = true,
+ canInvite = true,
+ searchQuery = "some query",
+ selectedUsers = persistentListOf(
+ aMatrixUser("@carol:server.org", "Carol")
+ ),
+ searchResults = SearchBarResultState.Results(
+ persistentListOf(
+ anInvitableUser(aMatrixUser("@alice:server.org")),
+ anInvitableUser(aMatrixUser("@bob:server.org", "Bob")),
+ anInvitableUser(aMatrixUser("@carol:server.org", "Carol"), isSelected = true),
+ anInvitableUser(aMatrixUser("@eve:server.org", "Eve"), isSelected = true, isAlreadyJoined = true),
+ anInvitableUser(aMatrixUser("@justin:server.org", "Justin"), isSelected = true, isAlreadyInvited = true),
+ )
+ )
+ ),
+ aDefaultInvitePeopleState(
+ isSearchActive = true,
+ canInvite = true,
+ searchQuery = "@alice:server.org",
+ selectedUsers = persistentListOf(
+ aMatrixUser("@carol:server.org", "Carol")
+ ),
+ searchResults = SearchBarResultState.Results(
+ persistentListOf(
+ anInvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
+ anInvitableUser(aMatrixUser("@bob:server.org", "Bob")),
+ )
+ )
+ ),
+ aDefaultInvitePeopleState(
+ isSearchActive = true,
+ canInvite = true,
+ searchQuery = "@alice:server.org",
+ searchResults = SearchBarResultState.Results(
+ persistentListOf(
+ anInvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
+ )
+ ),
+ showSearchLoader = true,
+ ),
+ aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
+ aDefaultInvitePeopleState(
+ canInvite = false,
+ selectedUsers = aMatrixUserList().toImmutableList(),
+ sendInvitesAction = AsyncAction.Loading,
+ ),
+ )
+}
+
+private fun anInvitableUser(
+ matrixUser: MatrixUser,
+ isSelected: Boolean = false,
+ isAlreadyJoined: Boolean = false,
+ isAlreadyInvited: Boolean = false,
+ isUnresolved: Boolean = false,
+) = InvitableUser(
+ matrixUser = matrixUser,
+ isSelected = isSelected,
+ isAlreadyJoined = isAlreadyJoined,
+ isAlreadyInvited = isAlreadyInvited,
+ isUnresolved = isUnresolved,
+)
+
+private fun aDefaultInvitePeopleState(
+ room: AsyncData = AsyncData.Success(Unit),
+ canInvite: Boolean = false,
+ searchQuery: String = "",
+ searchResults: SearchBarResultState> = SearchBarResultState.Initial(),
+ selectedUsers: ImmutableList = persistentListOf(),
+ isSearchActive: Boolean = false,
+ showSearchLoader: Boolean = false,
+ sendInvitesAction: AsyncAction = AsyncAction.Uninitialized,
+): DefaultInvitePeopleState {
+ return DefaultInvitePeopleState(
+ room = room,
+ canInvite = canInvite,
+ searchQuery = searchQuery,
+ searchResults = searchResults,
+ selectedUsers = selectedUsers,
+ isSearchActive = isSearchActive,
+ showSearchLoader = showSearchLoader,
+ sendInvitesAction = sendInvitesAction,
+ eventSink = {},
+ )
+}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.kt
new file mode 100644
index 00000000000..d2fd3f5edf1
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitableUser.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+data class InvitableUser(
+ val matrixUser: MatrixUser,
+ val isSelected: Boolean,
+ val isAlreadyJoined: Boolean,
+ val isAlreadyInvited: Boolean,
+ val isUnresolved: Boolean,
+)
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
new file mode 100644
index 00000000000..a8e056c7aac
--- /dev/null
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.async.AsyncFailure
+import io.element.android.libraries.designsystem.components.async.AsyncLoading
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.SearchBar
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.components.CheckableUserRow
+import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
+import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.matrix.ui.model.getBestName
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun InvitePeopleView(
+ state: DefaultInvitePeopleState,
+ modifier: Modifier = Modifier,
+) {
+ when (state.room) {
+ is AsyncData.Failure -> InvitePeopleViewError(state.room.error, modifier)
+ AsyncData.Uninitialized,
+ is AsyncData.Loading,
+ is AsyncData.Success -> InvitePeopleContentView(state, modifier)
+ }
+}
+
+@Composable
+private fun InvitePeopleViewError(
+ error: Throwable,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ AsyncFailure(
+ throwable = error,
+ onRetry = null,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+}
+
+@Composable
+private fun InvitePeopleContentView(
+ state: DefaultInvitePeopleState,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ InvitePeopleSearchBar(
+ modifier = Modifier.fillMaxWidth(),
+ query = state.searchQuery,
+ showLoader = state.showSearchLoader,
+ selectedUsers = state.selectedUsers,
+ state = state.searchResults,
+ active = state.isSearchActive,
+ onActiveChange = {
+ state.eventSink(
+ DefaultInvitePeopleEvents.OnSearchActiveChanged(
+ it
+ )
+ )
+ },
+ onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
+ onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
+ )
+
+ if (!state.isSearchActive) {
+ SelectedUsersRowList(
+ modifier = Modifier.fillMaxWidth(),
+ selectedUsers = state.selectedUsers,
+ autoScroll = true,
+ onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
+ contentPadding = PaddingValues(16.dp),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun InvitePeopleSearchBar(
+ query: String,
+ state: SearchBarResultState>,
+ showLoader: Boolean,
+ selectedUsers: ImmutableList,
+ active: Boolean,
+ onActiveChange: (Boolean) -> Unit,
+ onTextChange: (String) -> Unit,
+ onToggleUser: (MatrixUser) -> Unit,
+ modifier: Modifier = Modifier,
+ placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
+) {
+ SearchBar(
+ query = query,
+ onQueryChange = onTextChange,
+ active = active,
+ onActiveChange = onActiveChange,
+ modifier = modifier,
+ placeHolderTitle = placeHolderTitle,
+ contentPrefix = {
+ if (selectedUsers.isNotEmpty()) {
+ SelectedUsersRowList(
+ modifier = Modifier.fillMaxWidth(),
+ selectedUsers = selectedUsers,
+ autoScroll = true,
+ onUserRemove = onToggleUser,
+ contentPadding = PaddingValues(16.dp),
+ )
+ }
+ },
+ showBackButton = false,
+ resultState = state,
+ contentSuffix = {
+ if (showLoader) {
+ AsyncLoading()
+ }
+ },
+ resultHandler = { results ->
+ Text(
+ text = stringResource(id = CommonStrings.common_search_results),
+ style = ElementTheme.typography.fontBodyLgMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp)
+ )
+
+ LazyColumn {
+ itemsIndexed(results) { index, invitableUser ->
+ val invitedOrJoined = invitableUser.isAlreadyInvited || invitableUser.isAlreadyJoined
+ val isUnresolved = invitableUser.isUnresolved && !invitedOrJoined
+ val enabled = isUnresolved || !invitedOrJoined
+ val data = if (isUnresolved) {
+ CheckableUserRowData.Unresolved(
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
+ id = invitableUser.matrixUser.userId.value,
+ )
+ } else {
+ CheckableUserRowData.Resolved(
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
+ name = invitableUser.matrixUser.getBestName(),
+ subtext = when {
+ // If they're already invited or joined we show that information
+ invitableUser.isAlreadyJoined -> stringResource(R.string.screen_invite_users_already_a_member)
+ invitableUser.isAlreadyInvited -> stringResource(R.string.screen_invite_users_already_invited)
+ // Otherwise show the ID, unless that's already used for their name
+ invitableUser.matrixUser.displayName.isNullOrEmpty()
+ .not() -> invitableUser.matrixUser.userId.value
+ else -> null
+ }
+ )
+ }
+ CheckableUserRow(
+ checked = invitableUser.isSelected || invitedOrJoined,
+ enabled = enabled,
+ data = data,
+ onCheckedChange = { onToggleUser(invitableUser.matrixUser) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ },
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
+ ElementPreview {
+ InvitePeopleView(state = state)
+ }
diff --git a/features/invitepeople/impl/src/main/res/values-be/translations.xml b/features/invitepeople/impl/src/main/res/values-be/translations.xml
new file mode 100644
index 00000000000..7125d8b2e6a
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-be/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ужо ўдзельнік"
+ "Ужо запрасілі"
+
diff --git a/features/invitepeople/impl/src/main/res/values-bg/translations.xml b/features/invitepeople/impl/src/main/res/values-bg/translations.xml
new file mode 100644
index 00000000000..f5859809202
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-bg/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Вече е член"
+ "Вече е бил поканен"
+
diff --git a/features/invitepeople/impl/src/main/res/values-cs/translations.xml b/features/invitepeople/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 00000000000..fa5b3aa9a93
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Již členem"
+ "Již pozván(a)"
+
diff --git a/features/invitepeople/impl/src/main/res/values-cy/translations.xml b/features/invitepeople/impl/src/main/res/values-cy/translations.xml
new file mode 100644
index 00000000000..c58df3c541f
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-cy/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Eisoes yn aelod"
+ "Wedi gwahodd yn barod"
+
diff --git a/features/invitepeople/impl/src/main/res/values-da/translations.xml b/features/invitepeople/impl/src/main/res/values-da/translations.xml
new file mode 100644
index 00000000000..fbb1814e9f7
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-da/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allerede medlem"
+ "Allerede inviteret"
+
diff --git a/features/invitepeople/impl/src/main/res/values-de/translations.xml b/features/invitepeople/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 00000000000..182d9f289af
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Bereits Mitglied"
+ "Bereits eingeladen"
+
diff --git a/features/invitepeople/impl/src/main/res/values-el/translations.xml b/features/invitepeople/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 00000000000..3e3c1fd5d2c
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ήδη μέλος"
+ "Ήδη προσκεκλημένος"
+
diff --git a/features/invitepeople/impl/src/main/res/values-es/translations.xml b/features/invitepeople/impl/src/main/res/values-es/translations.xml
new file mode 100644
index 00000000000..e62bb211e52
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-es/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Ya eres miembro"
+ "Ya estás invitado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-et/translations.xml b/features/invitepeople/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 00000000000..44484d23c3c
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Sa juba oled jututoa liige"
+ "Sa juba oled kutse saanud"
+
diff --git a/features/invitepeople/impl/src/main/res/values-eu/translations.xml b/features/invitepeople/impl/src/main/res/values-eu/translations.xml
new file mode 100644
index 00000000000..a6aedd89cd4
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-eu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Kidea da dagoeneko"
+ "Lehendik ere gonbidatuta"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fa/translations.xml b/features/invitepeople/impl/src/main/res/values-fa/translations.xml
new file mode 100644
index 00000000000..544f01b8c85
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fa/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "از پیش عضو است"
+ "از پیش دعوت شده"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fi/translations.xml b/features/invitepeople/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 00000000000..e3479197195
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "On jo jäsen"
+ "On jo kutsuttu"
+
diff --git a/features/invitepeople/impl/src/main/res/values-fr/translations.xml b/features/invitepeople/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..dcc16f58cf3
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Déjà membre"
+ "Déjà invité(e)"
+
diff --git a/features/invitepeople/impl/src/main/res/values-hu/translations.xml b/features/invitepeople/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 00000000000..16f35b018c6
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Már tag"
+ "Már meghívták"
+
diff --git a/features/invitepeople/impl/src/main/res/values-in/translations.xml b/features/invitepeople/impl/src/main/res/values-in/translations.xml
new file mode 100644
index 00000000000..e112033a064
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-in/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Sudah menjadi anggota"
+ "Sudah diundang"
+
diff --git a/features/invitepeople/impl/src/main/res/values-it/translations.xml b/features/invitepeople/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 00000000000..979e42de1be
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Già membro"
+ "Già invitato"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ka/translations.xml b/features/invitepeople/impl/src/main/res/values-ka/translations.xml
new file mode 100644
index 00000000000..3a34c2edac9
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ka/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "უკვე წევრია"
+ "უკვე მოწვეულია"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ko/translations.xml b/features/invitepeople/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..19214c5a79e
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "이미 회원"
+ "이미 초대됨"
+
diff --git a/features/invitepeople/impl/src/main/res/values-lt/translations.xml b/features/invitepeople/impl/src/main/res/values-lt/translations.xml
new file mode 100644
index 00000000000..59c15290a1b
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-lt/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Jau narys"
+ "Jau pakviestas"
+
diff --git a/features/invitepeople/impl/src/main/res/values-nb/translations.xml b/features/invitepeople/impl/src/main/res/values-nb/translations.xml
new file mode 100644
index 00000000000..617b9271d5f
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-nb/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allerede medlem"
+ "Allerede invitert"
+
diff --git a/features/invitepeople/impl/src/main/res/values-nl/translations.xml b/features/invitepeople/impl/src/main/res/values-nl/translations.xml
new file mode 100644
index 00000000000..b978dcd9cce
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-nl/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Reeds lid"
+ "Reeds uitgenodigd"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pl/translations.xml b/features/invitepeople/impl/src/main/res/values-pl/translations.xml
new file mode 100644
index 00000000000..bfd537bb4bb
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pl/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Jest już członkiem"
+ "Już zaproszony"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml b/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml
new file mode 100644
index 00000000000..7ad049843bb
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pt-rBR/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Já é membro"
+ "Já foi convidado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-pt/translations.xml b/features/invitepeople/impl/src/main/res/values-pt/translations.xml
new file mode 100644
index 00000000000..a953d11f67a
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-pt/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Já é participante"
+ "Já foi convidado"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ro/translations.xml b/features/invitepeople/impl/src/main/res/values-ro/translations.xml
new file mode 100644
index 00000000000..f03be4b2631
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ro/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Deja membru"
+ "Deja invitat"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ru/translations.xml b/features/invitepeople/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 00000000000..0ae8eb792ce
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Уже зарегистрирован"
+ "Уже приглашены"
+
diff --git a/features/invitepeople/impl/src/main/res/values-sk/translations.xml b/features/invitepeople/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 00000000000..68e34b0adab
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Už ste členom"
+ "Už ste pozvaní"
+
diff --git a/features/invitepeople/impl/src/main/res/values-sv/translations.xml b/features/invitepeople/impl/src/main/res/values-sv/translations.xml
new file mode 100644
index 00000000000..fd670c9c1a4
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-sv/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Redan medlem"
+ "Redan inbjuden"
+
diff --git a/features/invitepeople/impl/src/main/res/values-tr/translations.xml b/features/invitepeople/impl/src/main/res/values-tr/translations.xml
new file mode 100644
index 00000000000..427a7f3f107
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-tr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Zaten üye"
+ "Zaten davet edildi"
+
diff --git a/features/invitepeople/impl/src/main/res/values-uk/translations.xml b/features/invitepeople/impl/src/main/res/values-uk/translations.xml
new file mode 100644
index 00000000000..da3ac9fe5bd
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-uk/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Уже учасник"
+ "Уже запрошені"
+
diff --git a/features/invitepeople/impl/src/main/res/values-ur/translations.xml b/features/invitepeople/impl/src/main/res/values-ur/translations.xml
new file mode 100644
index 00000000000..06e8cee4142
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-ur/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "پہلے سے ہی رکن"
+ "پہلے سے مدعو شدہ"
+
diff --git a/features/invitepeople/impl/src/main/res/values-uz/translations.xml b/features/invitepeople/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 00000000000..445a62d3188
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Allaqachon a\'zo"
+ "Allaqachon taklif qilingan"
+
diff --git a/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml b/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 00000000000..c8117ec283e
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "已是成員"
+ "已邀請"
+
diff --git a/features/invitepeople/impl/src/main/res/values-zh/translations.xml b/features/invitepeople/impl/src/main/res/values-zh/translations.xml
new file mode 100644
index 00000000000..b1e0e953f8f
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values-zh/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "已经是成员"
+ "已邀请"
+
diff --git a/features/invitepeople/impl/src/main/res/values/localazy.xml b/features/invitepeople/impl/src/main/res/values/localazy.xml
new file mode 100644
index 00000000000..d89ae92e759
--- /dev/null
+++ b/features/invitepeople/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,5 @@
+
+
+ "Already a member"
+ "Already invited"
+
diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
new file mode 100644
index 00000000000..388baf15e68
--- /dev/null
+++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
@@ -0,0 +1,574 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.invitepeople.impl
+
+import app.cash.turbine.ReceiveTurbine
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
+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.UserId
+import io.element.android.libraries.matrix.api.room.JoinedRoom
+import io.element.android.libraries.matrix.api.room.RoomMembersState
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.user.MatrixUser
+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_USER_ID
+import io.element.android.libraries.matrix.test.A_USER_ID_2
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
+import io.element.android.libraries.matrix.test.room.aRoomMember
+import io.element.android.libraries.matrix.test.room.aRoomMemberList
+import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.libraries.usersearch.api.UserSearchResult
+import io.element.android.libraries.usersearch.api.UserSearchResultState
+import io.element.android.libraries.usersearch.test.FakeUserRepository
+import io.element.android.services.apperror.api.AppErrorStateService
+import io.element.android.services.apperror.test.FakeAppErrorStateService
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+internal class DefaultInvitePeoplePresenterTest {
+ @get:Rule
+ val warmUpRule = WarmUpRule()
+
+ @Test
+ fun `present - initial state has no results and no search`() = runTest {
+ val presenter = createDefaultInvitePeoplePresenter()
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room).isEqualTo(AsyncData.Success(Unit))
+ assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
+ assertThat(initialState.isSearchActive).isFalse()
+ assertThat(initialState.canInvite).isFalse()
+ assertThat(initialState.searchQuery).isEmpty()
+
+ skipItems(1)
+ }
+ }
+
+ @Test
+ fun `present - updates search active state`() = runTest {
+ val presenter = createDefaultInvitePeoplePresenter()
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
+
+ val resultState = awaitItem()
+ assertThat(resultState.isSearchActive).isTrue()
+ resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query")
+ resultState.eventSink(InvitePeopleEvents.CloseSearch)
+ skipItems(1)
+ awaitItemAsDefault().also {
+ assertThat(it.isSearchActive).isFalse()
+ assertThat(it.searchQuery).isEmpty()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles empty result list`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
+ skipItems(3)
+ awaitItemAsDefault().also { state ->
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
+ assertThat(state.showSearchLoader).isTrue()
+ }
+ repository.emitState(results = emptyList(), isSearching = false)
+ awaitItemAsDefault().also { state ->
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
+ assertThat(state.showSearchLoader).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles user results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList())
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val expectedUsers = aMatrixUserList()
+ val users = resultState.searchResults.users()
+ expectedUsers.forEachIndexed { index, matrixUser ->
+ assertThat(users[index].matrixUser).isEqualTo(matrixUser)
+ // All users are joined or invited
+ if (users[index].isAlreadyInvited) {
+ assertThat(users[index].isAlreadyJoined).isFalse()
+ } else {
+ assertThat(users[index].isAlreadyJoined).isTrue()
+ }
+ assertThat(users[index].isSelected).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles membership state of existing users`() = runTest {
+ val userList = aMatrixUserList()
+ val joinedUser = userList[0]
+ val invitedUser = userList[1]
+
+ val repository = FakeUserRepository()
+ val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ roomMembersState = RoomMembersState.Ready(
+ persistentListOf(
+ aRoomMember(
+ userId = joinedUser.userId,
+ membership = RoomMembershipState.JOIN
+ ),
+ aRoomMember(
+ userId = invitedUser.userId,
+ membership = RoomMembershipState.INVITE
+ ),
+ )
+ ),
+ coroutineDispatchers = coroutineDispatchers,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList())
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ // The result that matches a user with JOINED membership is marked as such
+ val userWhoShouldBeJoined = users.find { it.matrixUser == joinedUser }
+ assertThat(userWhoShouldBeJoined).isNotNull()
+ assertThat(userWhoShouldBeJoined?.isAlreadyJoined).isTrue()
+ assertThat(userWhoShouldBeJoined?.isAlreadyInvited).isFalse()
+
+ // The result that matches a user with INVITED membership is marked as such
+ val userWhoShouldBeInvited = users.find { it.matrixUser == invitedUser }
+ assertThat(userWhoShouldBeInvited).isNotNull()
+ assertThat(userWhoShouldBeInvited?.isAlreadyJoined).isFalse()
+ assertThat(userWhoShouldBeInvited?.isAlreadyInvited).isTrue()
+
+ // All other users are neither joined nor invited
+ val otherUsers = users.minus(userWhoShouldBeInvited!!).minus(userWhoShouldBeJoined!!)
+ assertThat(otherUsers.none { it.isAlreadyInvited }).isTrue()
+ assertThat(otherUsers.none { it.isAlreadyJoined }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - performs search and handles unresolved results`() = runTest {
+ val userList = aMatrixUserList()
+ val joinedUser = userList[0]
+ val invitedUser = userList[1]
+
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ roomMembersState =
+ RoomMembersState.Ready(
+ persistentListOf(
+ aRoomMember(
+ userId = joinedUser.userId,
+ membership = RoomMembershipState.JOIN
+ ),
+ aRoomMember(
+ userId = invitedUser.userId,
+ membership = RoomMembershipState.INVITE
+ ),
+ )
+ ),
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+
+ val unresolvedUser =
+ UserSearchResult(aMatrixUser(id = A_USER_ID.value), isUnresolved = true)
+ repository.emitState(listOf(unresolvedUser) + aMatrixUserList().map {
+ UserSearchResult(
+ it
+ )
+ })
+ skipItems(1)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ val userWhoShouldBeUnresolved = users.first()
+ assertThat(userWhoShouldBeUnresolved.isUnresolved).isTrue()
+
+ // All other users are neither joined nor invited
+ val otherUsers = users.minus(userWhoShouldBeUnresolved)
+ assertThat(otherUsers.none { it.isUnresolved }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggle users updates selected user state`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers()
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ // When we toggle a user not in the list, they are added
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser())
+
+ // Toggling a different user also adds them
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(
+ aMatrixUser(),
+ aMatrixUser(id = A_USER_ID_2.value)
+ )
+
+ // Toggling the first user removes them
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
+ }
+ }
+
+ @Test
+ fun `present - selected users appear as such in search results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ val selectedUser = aMatrixUser()
+
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(2)
+
+ val resultState = awaitItemAsDefault()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ // The one user we have previously toggled is marked as selected
+ val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldBeSelectedUser).isNotNull()
+ assertThat(shouldBeSelectedUser?.isSelected).isTrue()
+
+ // And no others are
+ val allOtherUsers = users.minus(shouldBeSelectedUser!!)
+ assertThat(allOtherUsers.none { it.isSelected }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggling a user updates existing search results`() = runTest {
+ val repository = FakeUserRepository()
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ val selectedUser = aMatrixUser()
+
+ // Given a query is made
+ initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ awaitItemAsDefault().also { state ->
+ // selectedUser is not selected
+ assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ val users = state.searchResults.users()
+ val shouldNotBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldNotBeSelectedUser).isNotNull()
+ assertThat(shouldNotBeSelectedUser?.isSelected).isFalse()
+ }
+
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ val users = resultState.searchResults.users()
+
+ // The one user we have now toggled is marked as selected
+ val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser }
+ assertThat(shouldBeSelectedUser).isNotNull()
+ assertThat(shouldBeSelectedUser?.isSelected).isTrue()
+
+ // And no others are
+ val allOtherUsers = users.minus(shouldBeSelectedUser!!)
+ assertThat(allOtherUsers.none { it.isSelected }).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - toggling a user and send invite success`() = runTest {
+ val repository = FakeUserRepository()
+ val inviteUserResult = lambdaRecorder> { userId: UserId ->
+ Result.success(Unit)
+ }
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ inviteUserResult = inviteUserResult,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+ val selectedUser = aMatrixUser()
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ // Send invites
+ initialState.eventSink(InvitePeopleEvents.SendInvites)
+
+ // Can't invite in the loading state
+ awaitItem().run {
+ assertThat(sendInvitesAction.isLoading()).isTrue()
+ assertThat(canInvite).isFalse()
+ }
+
+ delay(1_000)
+ inviteUserResult.assertions().isCalledOnce().with(
+ value(selectedUser.userId)
+ )
+
+ // Can invite again once the action is finished
+ awaitItem().run {
+ assertThat(sendInvitesAction.isReady()).isTrue()
+ assertThat(canInvite).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun `present - toggling a user and send invite error`() = runTest {
+ val repository = FakeUserRepository()
+ val inviteUserResult = lambdaRecorder> { _: UserId ->
+ Result.failure(AN_EXCEPTION)
+ }
+ val showErrorResResult = lambdaRecorder { _, _ -> }
+ val presenter = createDefaultInvitePeoplePresenter(
+ userRepository = repository,
+ inviteUserResult = inviteUserResult,
+ coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
+ appErrorStateService = FakeAppErrorStateService(
+ showErrorResResult = showErrorResResult,
+ )
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ skipItems(1)
+ val selectedUser = aMatrixUser()
+ repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
+ skipItems(1)
+ // And then a user is toggled
+ initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ skipItems(1)
+ val resultState = awaitItemAsDefault()
+ // The results are updated...
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+ // Send invites
+ initialState.eventSink(InvitePeopleEvents.SendInvites)
+
+ // Can't invite in the loading state
+ awaitItem().run {
+ assertThat(sendInvitesAction.isLoading()).isTrue()
+ assertThat(canInvite).isFalse()
+ }
+
+ delay(1_000)
+ inviteUserResult.assertions().isCalledOnce().with(
+ value(selectedUser.userId)
+ )
+ showErrorResResult.assertions()
+ .isCalledOnce()
+ .with(
+ value(CommonStrings.common_unable_to_invite_title),
+ value(CommonStrings.common_unable_to_invite_message)
+ )
+
+ // Can invite again once the action is finished
+ awaitItem().run {
+ assertThat(sendInvitesAction.isReady()).isTrue()
+ assertThat(canInvite).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient`() = runTest {
+ val matrixClient = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, FakeJoinedRoom())
+ }
+ val presenter = createDefaultInvitePeoplePresenter(
+ joinedRoom = null,
+ roomId = A_ROOM_ID,
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room.isLoading()).isTrue()
+ val finalState = awaitItemAsDefault()
+ assertThat(finalState.room).isEqualTo(AsyncData.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient - error case`() = runTest {
+ val matrixClient = FakeMatrixClient()
+ val presenter = createDefaultInvitePeoplePresenter(
+ joinedRoom = null,
+ roomId = A_ROOM_ID,
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItemAsDefault()
+ assertThat(initialState.room.isLoading()).isTrue()
+ val finalState = awaitItemAsDefault()
+ assertThat(finalState.room.errorOrNull()?.message).isEqualTo("Room not found")
+ }
+ }
+
+ private suspend fun FakeUserRepository.emitStateWithUsers(
+ users: List,
+ isSearching: Boolean = false
+ ) {
+ emitState(
+ results = users.map { UserSearchResult(it) },
+ isSearching = isSearching,
+ )
+ }
+
+ private suspend fun FakeUserRepository.emitState(
+ results: List,
+ isSearching: Boolean = false
+ ) {
+ val state = UserSearchResultState(
+ results = results,
+ isSearching = isSearching
+ )
+ emitState(state)
+ }
+
+ private fun SearchBarResultState>.users() =
+ (this as? SearchBarResultState.Results>)?.results.orEmpty()
+}
+
+private suspend fun ReceiveTurbine.awaitItemAsDefault(): DefaultInvitePeopleState {
+ return awaitItem() as DefaultInvitePeopleState
+}
+
+fun TestScope.createDefaultInvitePeoplePresenter(
+ roomMembersState: RoomMembersState = RoomMembersState.Ready(aRoomMemberList()),
+ inviteUserResult: (UserId) -> Result = { lambdaError() },
+ joinedRoom: JoinedRoom? = FakeJoinedRoom(
+ inviteUserResult = inviteUserResult,
+ ).apply {
+ givenRoomMembersState(roomMembersState)
+ },
+ roomId: RoomId = A_ROOM_ID,
+ userRepository: UserRepository = FakeUserRepository(),
+ coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
+ appErrorStateService: AppErrorStateService = FakeAppErrorStateService(),
+ matrixClient: MatrixClient = FakeMatrixClient(),
+): DefaultInvitePeoplePresenter {
+ return DefaultInvitePeoplePresenter(
+ joinedRoom = joinedRoom,
+ roomId = roomId,
+ userRepository = userRepository,
+ coroutineDispatchers = coroutineDispatchers,
+ sessionCoroutineScope = backgroundScope,
+ appErrorStateService = appErrorStateService,
+ matrixClient = matrixClient,
+ )
+}
diff --git a/features/joinroom/api/build.gradle.kts b/features/joinroom/api/build.gradle.kts
index 10c33410d45..6ba5f9c7619 100644
--- a/features/joinroom/api/build.gradle.kts
+++ b/features/joinroom/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt
index 9af47039499..4d61f27763a 100644
--- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt
+++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -18,7 +19,11 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import java.util.Optional
interface JoinRoomEntryPoint : FeatureEntryPoint {
- fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: Inputs,
+ ): Node
data class Inputs(
val roomId: RoomId,
diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts
index e195efd1bca..71acdad1289 100644
--- a/features/joinroom/impl/build.gradle.kts
+++ b/features/joinroom/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,7 +23,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.joinroom.api)
@@ -38,16 +40,9 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.appconfig)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs, true)
testImplementation(projects.features.invite.test)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.preferences.test)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+ testImplementation(projects.libraries.previewutils)
}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt
index e28b2affe86..2343860aab2 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,15 +10,18 @@ package io.element.android.features.joinroom.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint {
- override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node {
+class DefaultJoinRoomEntryPoint : JoinRoomEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: JoinRoomEntryPoint.Inputs,
+ ): Node {
return parentNode.createNode(
buildContext = buildContext,
plugins = listOf(inputs)
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
index 323b556c38d..51245dd8f63 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
index ecd4c920c8d..93f8f534125 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,9 +17,9 @@ import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
@@ -30,7 +31,8 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class JoinRoomFlowNode @AssistedInject constructor(
+@AssistedInject
+class JoinRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
presenterFactory: JoinRoomPresenter.Factory,
@@ -63,7 +65,11 @@ class JoinRoomFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
- is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(this, buildContext, navTarget.inviteData)
+ is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(
+ parentNode = this,
+ buildContext = buildContext,
+ inviteData = navTarget.inviteData,
+ )
NavTarget.Root -> rootNode(buildContext)
}
}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
index 6206c2426a3..49c5ffb85f2 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -20,9 +21,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
+import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@@ -34,27 +36,30 @@ 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.runUpdatingState
-import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.core.meta.BuildMeta
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.RoomIdOrAlias
-import io.element.android.libraries.matrix.api.exception.ClientException
-import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
-import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
+import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender
+import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
-class JoinRoomPresenter @AssistedInject constructor(
+@AssistedInject
+class JoinRoomPresenter(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional,
@@ -69,7 +74,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private val buildMeta: BuildMeta,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter {
- interface Factory {
+ fun interface Factory {
fun create(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
@@ -79,6 +84,8 @@ class JoinRoomPresenter @AssistedInject constructor(
): JoinRoomPresenter
}
+ private val spaceList = matrixClient.spaceService.spaceRoomList(roomId)
+
@Composable
override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope()
@@ -86,76 +93,54 @@ class JoinRoomPresenter @AssistedInject constructor(
val roomInfo by remember {
matrixClient.getRoomInfoFlow(roomId)
}.collectAsState(initial = Optional.empty())
+ val spaceRoom by spaceList.currentSpaceFlow.collectAsState()
val joinAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val cancelKnockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val forgetRoomAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var knockMessage by rememberSaveable { mutableStateOf("") }
var isDismissingContent by remember { mutableStateOf(false) }
- val hideInviteAvatars by remember {
- matrixClient
- .mediaPreviewService()
- .mediaPreviewConfigFlow
- .mapState { config -> config.hideInviteAvatar }
- }.collectAsState()
+ val hideInviteAvatars by matrixClient.rememberHideInvitesAvatar()
val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() }
- val contentState by produceState(
- initialValue = ContentState.Loading,
- key1 = roomInfo,
- key2 = retryCount,
- key3 = isDismissingContent,
- ) {
+ var contentState by remember {
+ mutableStateOf(ContentState.Loading)
+ }
+ LaunchedEffect(roomInfo, retryCount, isDismissingContent, spaceRoom) {
when {
- isDismissingContent -> value = ContentState.Dismissing
+ isDismissingContent -> contentState = ContentState.Dismissing
roomInfo.isPresent -> {
val notJoinedRoom = matrixClient.getRoomPreview(roomIdOrAlias, serverNames).getOrNull()
- val (sender, reason) = when (roomInfo.get().currentUserMembership) {
- CurrentUserMembership.BANNED -> {
- // Workaround to get info about the sender for banned rooms
- // TODO re-do this once we have a better API in the SDK
- val membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull()
- membershipDetails?.senderMember to membershipDetails?.currentUserMember?.membershipChangeReason
- }
- CurrentUserMembership.INVITED -> {
- roomInfo.get().inviter to null
- }
- else -> null to null
- }
+ val membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull()
val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers
- value = roomInfo.get().toContentState(
- membershipSender = sender,
+ contentState = roomInfo.get().toContentState(
joinedMembersCountOverride = joinedMembersCountOverride,
- reason = reason,
+ membershipDetails = membershipDetails,
+ childrenCount = spaceRoom.getOrNull()?.childrenCount,
)
}
+ spaceRoom.isPresent -> {
+ val spaceRoom = spaceRoom.get()
+ // Only use this state when space is not locally known
+ contentState = if (spaceRoom.state != null) {
+ ContentState.Loading
+ } else {
+ spaceRoom.toContentState()
+ }
+ }
roomDescription.isPresent -> {
- value = roomDescription.get().toContentState()
+ contentState = roomDescription.get().toContentState()
}
else -> {
- value = ContentState.Loading
+ contentState = ContentState.Loading
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
- value = result.fold(
+ contentState = result.fold(
onSuccess = { preview ->
- val membershipInfo = when (preview.previewInfo.membership) {
- CurrentUserMembership.INVITED,
- CurrentUserMembership.BANNED,
- CurrentUserMembership.KNOCKED -> {
- preview.membershipDetails().getOrNull()
- }
- else -> null
- }
- preview.previewInfo.toContentState(
- senderMember = membershipInfo?.senderMember,
- reason = membershipInfo?.currentUserMember?.membershipChangeReason,
- )
+ val membershipDetails = preview.membershipDetails().getOrNull()
+ preview.previewInfo.toContentState(membershipDetails)
},
onFailure = { throwable ->
- if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
- ContentState.UnknownRoom
- } else {
- ContentState.Failure(throwable)
- }
+ ContentState.UnknownRoom
}
)
}
@@ -167,7 +152,7 @@ class JoinRoomPresenter @AssistedInject constructor(
contentState.markRoomInviteAsSeen()
}
- fun handleEvents(event: JoinRoomEvents) {
+ fun handleEvent(event: JoinRoomEvents) {
when (event) {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
@@ -213,7 +198,7 @@ class JoinRoomPresenter @AssistedInject constructor(
knockMessage = knockMessage,
hideInviteAvatars = hideInviteAvatars,
canReportRoom = canReportRoom,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
@@ -256,30 +241,56 @@ class JoinRoomPresenter @AssistedInject constructor(
}
}
-private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState {
+private fun RoomPreviewInfo.toContentState(membershipDetails: RoomMembershipDetails?): ContentState {
return ContentState.Loaded(
roomId = roomId,
name = name,
topic = topic,
alias = canonicalAlias,
numberOfMembers = numberOfJoinedMembers,
- isDm = false,
- roomType = roomType,
roomAvatarUrl = avatarUrl,
- joinAuthorisationStatus = when (membership) {
- CurrentUserMembership.INVITED -> {
- JoinAuthorisationStatus.IsInvited(
- inviteData = toInviteData(),
- inviteSender = senderMember?.toInviteSender()
- )
- }
- CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
- CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
- else -> joinRule.toJoinAuthorisationStatus()
+ joinAuthorisationStatus = computeJoinAuthorisationStatus(
+ membership,
+ membershipDetails,
+ joinRule,
+ { toInviteData() }
+ ),
+ joinRule = joinRule,
+ details = when (roomType) {
+ is RoomType.Other,
+ RoomType.Room -> LoadedDetails.Room(
+ isDm = false,
+ )
+ RoomType.Space -> LoadedDetails.Space(
+ childrenCount = 0,
+ heroes = persistentListOf(),
+ )
}
)
}
+private fun SpaceRoom.toContentState(): ContentState {
+ return ContentState.Loaded(
+ roomId = roomId,
+ name = displayName,
+ topic = topic,
+ alias = canonicalAlias,
+ numberOfMembers = numJoinedMembers.toLong(),
+ roomAvatarUrl = avatarUrl,
+ joinAuthorisationStatus = computeJoinAuthorisationStatus(
+ membership = state,
+ membershipDetails = null,
+ joinRule = joinRule,
+ inviteData = { toInviteData() }
+ ),
+ joinRule = joinRule,
+ details = LoadedDetails.Space(
+ childrenCount = childrenCount,
+ heroes = heroes.toImmutableList(),
+ )
+ )
+}
+
@VisibleForTesting
internal fun RoomDescription.toContentState(): ContentState {
return ContentState.Loaded(
@@ -288,22 +299,29 @@ internal fun RoomDescription.toContentState(): ContentState {
topic = topic,
alias = alias,
numberOfMembers = numberOfMembers,
- isDm = false,
- roomType = RoomType.Room,
roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when (joinRule) {
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
RoomDescription.JoinRule.PUBLIC -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown
- }
+ },
+ joinRule = when (joinRule) {
+ RoomDescription.JoinRule.KNOCK -> JoinRule.Knock
+ RoomDescription.JoinRule.PUBLIC -> JoinRule.Public
+ RoomDescription.JoinRule.RESTRICTED -> JoinRule.Restricted(persistentListOf())
+ RoomDescription.JoinRule.KNOCK_RESTRICTED -> JoinRule.KnockRestricted(persistentListOf())
+ RoomDescription.JoinRule.INVITE -> JoinRule.Invite
+ RoomDescription.JoinRule.UNKNOWN -> null
+ },
+ details = LoadedDetails.Room(isDm = false)
)
}
@VisibleForTesting
internal fun RoomInfo.toContentState(
- membershipSender: RoomMember?,
joinedMembersCountOverride: Long?,
- reason: String?,
+ membershipDetails: RoomMembershipDetails?,
+ childrenCount: Int?,
): ContentState {
return ContentState.Loaded(
roomId = id,
@@ -311,22 +329,47 @@ internal fun RoomInfo.toContentState(
topic = topic,
alias = canonicalAlias,
numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount,
- isDm = isDm,
- roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl,
- joinAuthorisationStatus = when (currentUserMembership) {
- CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
- inviteData = toInviteData(),
- inviteSender = membershipSender?.toInviteSender(),
+ joinAuthorisationStatus = computeJoinAuthorisationStatus(
+ membership = currentUserMembership,
+ membershipDetails = membershipDetails,
+ joinRule = joinRule,
+ inviteData = { toInviteData() }
+ ),
+ joinRule = joinRule,
+ details = if (isSpace) {
+ LoadedDetails.Space(
+ childrenCount = childrenCount ?: 0,
+ heroes = heroes,
+ )
+ } else {
+ LoadedDetails.Room(
+ isDm = isDm,
)
- CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
- banSender = membershipSender?.toInviteSender(),
- reason = reason,
+ },
+ )
+}
+
+private fun computeJoinAuthorisationStatus(
+ membership: CurrentUserMembership?,
+ membershipDetails: RoomMembershipDetails?,
+ joinRule: JoinRule?,
+ inviteData: () -> InviteData,
+): JoinAuthorisationStatus {
+ return when (membership) {
+ CurrentUserMembership.INVITED -> {
+ JoinAuthorisationStatus.IsInvited(
+ inviteData = inviteData(),
+ inviteSender = membershipDetails?.senderMember?.toInviteSender()
)
- CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
- else -> joinRule.toJoinAuthorisationStatus()
}
- )
+ CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
+ membershipDetails?.senderMember?.toInviteSender(),
+ membershipDetails?.membershipChangeReason
+ )
+ CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
+ else -> joinRule.toJoinAuthorisationStatus()
+ }
}
private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
index 5b9f8007a3a..e109bb1315f 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,13 +17,14 @@ 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.RoomIdOrAlias
-import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom
+import io.element.android.libraries.matrix.api.room.join.JoinRule
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
+import kotlinx.collections.immutable.ImmutableList
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
-@Immutable
data class JoinRoomState(
val roomIdOrAlias: RoomIdOrAlias,
val contentState: ContentState,
@@ -41,9 +43,6 @@ data class JoinRoomState(
val joinAuthorisationStatus = when (contentState) {
is ContentState.Loaded -> {
when {
- contentState.roomType == RoomType.Space -> {
- JoinAuthorisationStatus.IsSpace(applicationName)
- }
isJoinActionUnauthorized -> {
JoinAuthorisationStatus.Unauthorized
}
@@ -77,12 +76,13 @@ sealed interface ContentState {
val topic: String?,
val alias: RoomAlias?,
val numberOfMembers: Long?,
- val isDm: Boolean,
- val roomType: RoomType,
val roomAvatarUrl: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus,
+ val joinRule: JoinRule?,
+ val details: LoadedDetails,
) : ContentState {
val showMemberCount = numberOfMembers != null
+ val isSpace = details is LoadedDetails.Space
fun avatarData(size: AvatarSize): AvatarData {
return AvatarData(
@@ -95,9 +95,20 @@ sealed interface ContentState {
}
}
+@Immutable
+sealed interface LoadedDetails {
+ data class Room(
+ val isDm: Boolean,
+ ) : LoadedDetails
+
+ data class Space(
+ val childrenCount: Int,
+ val heroes: ImmutableList,
+ ) : LoadedDetails
+}
+
sealed interface JoinAuthorisationStatus {
data object None : JoinAuthorisationStatus
- data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
data object IsKnocked : JoinAuthorisationStatus
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
index 8f891fe6453..7e5142321a1 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,8 +10,8 @@ package io.element.android.features.joinroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.InviteData
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
+import io.element.android.features.invite.api.acceptdecline.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
@@ -20,9 +21,11 @@ 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.exception.ClientException
-import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom
+import io.element.android.libraries.matrix.api.room.join.JoinRule
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
+import kotlinx.collections.immutable.toImmutableList
open class JoinRoomStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -77,13 +80,17 @@ open class JoinRoomStateProvider : PreviewParameterProvider {
name = "A space",
alias = null,
topic = "This is the topic of a space",
- roomType = RoomType.Space,
+ details = aLoadedDetailsSpace(
+ childrenCount = 42,
+ ),
)
),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A DM",
- isDm = true,
+ details = aLoadedDetailsRoom(
+ isDm = true,
+ ),
)
),
aJoinRoomState(
@@ -156,20 +163,34 @@ fun aLoadedContentState(
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null,
- isDm: Boolean = false,
- roomType: RoomType = RoomType.Room,
roomAvatarUrl: String? = null,
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
+ joinRule: JoinRule? = null,
+ details: LoadedDetails = aLoadedDetailsRoom(isDm = false),
) = ContentState.Loaded(
roomId = roomId,
name = name,
alias = alias,
topic = topic,
numberOfMembers = numberOfMembers,
- isDm = isDm,
- roomType = roomType,
roomAvatarUrl = roomAvatarUrl,
- joinAuthorisationStatus = joinAuthorisationStatus
+ joinAuthorisationStatus = joinAuthorisationStatus,
+ joinRule = joinRule,
+ details = details,
+)
+
+fun aLoadedDetailsRoom(
+ isDm: Boolean = false,
+) = LoadedDetails.Room(
+ isDm = isDm
+)
+
+fun aLoadedDetailsSpace(
+ childrenCount: Int = 0,
+ heroes: List = emptyList(),
+) = LoadedDetails.Space(
+ childrenCount = childrenCount,
+ heroes = heroes.toImmutableList()
)
fun aJoinRoomState(
@@ -199,16 +220,6 @@ fun aJoinRoomState(
eventSink = eventSink
)
-internal fun anAcceptDeclineInviteState(
- acceptAction: AsyncAction = AsyncAction.Uninitialized,
- declineAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
-) = AcceptDeclineInviteState(
- acceptAction = acceptAction,
- declineAction = declineAction,
- eventSink = eventSink,
-)
-
internal fun anInviteSender(
userId: UserId = UserId("@bob:domain"),
displayName: String = "Bob",
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
index cdeac0afe94..8992b920560 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
@@ -1,27 +1,27 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -38,6 +38,7 @@ 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.compound.tokens.generated.CompoundIcons
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
@@ -46,7 +47,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAt
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
-import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.MembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.Announcement
@@ -65,14 +66,21 @@ 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.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconSource
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.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.designsystem.theme.placeholderBackground
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-import io.element.android.libraries.matrix.ui.components.InviteSenderView
+import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
+import io.element.android.libraries.matrix.ui.components.SpaceInfoRow
+import io.element.android.libraries.matrix.ui.components.SpaceMembersView
+import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.persistentListOf
@Composable
fun JoinRoomView(
@@ -92,7 +100,7 @@ fun JoinRoomView(
containerColor = Color.Transparent,
contentPadding = PaddingValues(
horizontal = 16.dp,
- vertical = 32.dp
+ vertical = 24.dp
),
topBar = {
JoinRoomTopBar(
@@ -220,12 +228,14 @@ private fun JoinRoomFooter(
onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
+ leadingIcon = IconSource.Vector(CompoundIcons.Close())
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
+ leadingIcon = IconSource.Vector(CompoundIcons.Check())
)
}
Spacer(modifier = Modifier.height(24.dp))
@@ -278,7 +288,6 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack)
- is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack)
JoinAuthorisationStatus.None -> Unit
}
}
@@ -358,28 +367,6 @@ private fun JoinRestrictedFooter(
}
}
-@Composable
-private fun UnsupportedSpaceFooter(
- applicationName: String,
- onGoBack: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- Column(modifier = modifier) {
- Announcement(
- title = stringResource(R.string.screen_join_room_space_not_supported_title),
- description = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
- type = AnnouncementType.Informative(),
- )
- Spacer(Modifier.height(24.dp))
- Button(
- text = stringResource(CommonStrings.action_ok),
- onClick = onGoBack,
- modifier = Modifier.fillMaxWidth(),
- size = ButtonSize.Large,
- )
- }
-}
-
@Composable
private fun JoinRoomContent(
roomIdOrAlias: RoomIdOrAlias,
@@ -397,19 +384,40 @@ private fun JoinRoomContent(
IsKnockedLoadedContent()
}
else -> {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
- if (inviteSender != null) {
- InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages)
- Spacer(modifier = Modifier.height(32.dp))
- }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
DefaultLoadedContent(
- modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
- knockMessage = knockMessage,
hideAvatarImage = hideAvatarsImages,
- onKnockMessageUpdate = onKnockMessageUpdate
)
+ when (contentState.joinAuthorisationStatus) {
+ is JoinAuthorisationStatus.IsInvited -> {
+ val inviteSender = contentState.joinAuthorisationStatus.inviteSender
+ if (inviteSender != null) {
+ Spacer(Modifier.height(16.dp))
+ InvitedByView(inviteSender, hideAvatarsImages)
+ }
+ }
+ is JoinAuthorisationStatus.CanKnock -> {
+ Spacer(modifier = Modifier.height(24.dp))
+ val supportingText = if (knockMessage.isNotEmpty()) {
+ "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
+ } else {
+ stringResource(R.string.screen_join_room_knock_message_description)
+ }
+ TextField(
+ value = knockMessage,
+ onValueChange = onKnockMessageUpdate,
+ maxLines = 3,
+ minLines = 3,
+ modifier = Modifier.fillMaxWidth(),
+ supportingText = supportingText
+ )
+ }
+ else -> Unit
+ }
}
}
}
@@ -422,6 +430,45 @@ private fun JoinRoomContent(
}
}
+@Composable
+private fun InvitedByView(
+ sender: InviteSender,
+ hideAvatarImage: Boolean,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.screen_join_room_invited_by),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary
+ )
+ Spacer(Modifier.height(8.dp))
+ Avatar(
+ avatarData = sender.avatarData,
+ avatarType = AvatarType.User,
+ hideImage = hideAvatarImage,
+ forcedAvatarSize = AvatarSize.RoomPreviewInviter.dp
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = sender.displayName,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = sender.userId.value,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary
+ )
+ }
+}
+
@Composable
private fun UnknownRoomContent(
modifier: Modifier = Modifier
@@ -429,7 +476,21 @@ private fun UnknownRoomContent(
RoomPreviewOrganism(
modifier = modifier,
avatar = {
- Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp))
+ Box(
+ modifier = Modifier
+ .size(AvatarSize.RoomPreviewHeader.dp)
+ .background(
+ color = ElementTheme.colors.placeholderBackground,
+ shape = CircleShape
+ )
+ ) {
+ Icon(
+ modifier = Modifier.align(Alignment.Center),
+ tint = ElementTheme.colors.iconPrimary,
+ imageVector = CompoundIcons.VisibilityOff(),
+ contentDescription = null,
+ )
+ }
},
title = {
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
@@ -448,7 +509,7 @@ private fun IncompleteContent(
RoomPreviewOrganism(
modifier = modifier,
avatar = {
- PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
+ PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
},
title = {
when (roomIdOrAlias) {
@@ -471,43 +532,32 @@ private fun IncompleteContent(
@Composable
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
- BoxWithConstraints(
- modifier = modifier
- .fillMaxHeight()
- .padding(horizontal = 16.dp),
- contentAlignment = Alignment.Center,
- ) {
- IconTitleSubtitleMolecule(
- modifier = Modifier.sizeIn(minHeight = maxHeight * 0.7f),
- iconStyle = BigIcon.Style.SuccessSolid,
- title = stringResource(R.string.screen_join_room_knock_sent_title),
- subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
- )
- }
+ IconTitleSubtitleMolecule(
+ modifier = modifier.padding(horizontal = 8.dp),
+ iconStyle = BigIcon.Style.SuccessSolid,
+ title = stringResource(R.string.screen_join_room_knock_sent_title),
+ subTitle = stringResource(R.string.screen_join_room_knock_sent_description),
+ )
}
@Composable
private fun DefaultLoadedContent(
contentState: ContentState.Loaded,
- knockMessage: String,
hideAvatarImage: Boolean,
- onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
Avatar(
- contentState.avatarData(AvatarSize.RoomHeader),
+ contentState.avatarData(AvatarSize.RoomPreviewHeader),
hideImage = hideAvatarImage,
- avatarType = AvatarType.Room(),
+ avatarType = if (contentState.isSpace) AvatarType.Space() else AvatarType.Room(),
)
},
title = {
if (contentState.name != null) {
- RoomPreviewTitleAtom(
- title = contentState.name,
- )
+ RoomPreviewTitleAtom(title = contentState.name)
} else {
RoomPreviewTitleAtom(
title = stringResource(id = CommonStrings.common_no_room_name),
@@ -516,37 +566,29 @@ private fun DefaultLoadedContent(
}
},
subtitle = {
- if (contentState.alias != null) {
- RoomPreviewSubtitleAtom(contentState.alias.value)
+ when {
+ contentState.details is LoadedDetails.Space -> {
+ SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
+ }
+ contentState.alias != null -> {
+ RoomPreviewSubtitleAtom(contentState.alias.value)
+ }
}
},
description = {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- RoomPreviewDescriptionAtom(contentState.topic ?: "")
- if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
- Spacer(modifier = Modifier.height(24.dp))
- val supportingText = if (knockMessage.isNotEmpty()) {
- "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
- } else {
- stringResource(R.string.screen_join_room_knock_message_description)
- }
- TextField(
- value = knockMessage,
- onValueChange = onKnockMessageUpdate,
- maxLines = 3,
- minLines = 3,
- modifier = Modifier.fillMaxWidth(),
- supportingText = supportingText
- )
- }
- }
+ RoomPreviewDescriptionAtom(
+ contentState.topic ?: "",
+ maxLines = if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanJoin) Int.MAX_VALUE else 2
+ )
},
memberCount = {
if (contentState.showMemberCount) {
- RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
+ val membersCount = contentState.numberOfMembers?.toInt() ?: 0
+ if (contentState.isSpace) {
+ SpaceMembersView(persistentListOf(), membersCount)
+ } else {
+ MembersCountMolecule(memberCount = membersCount)
+ }
}
}
)
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt
index 7826a819c68..11614dc7370 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt
@@ -1,24 +1,24 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.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 CancelKnockRoom {
suspend operator fun invoke(roomId: RoomId): Result
}
@ContributesBinding(SessionScope::class)
-class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom {
+class DefaultCancelKnockRoom(private val client: MatrixClient) : CancelKnockRoom {
override suspend fun invoke(roomId: RoomId): Result {
return client
.getRoom(roomId)
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt
index ac17e2c3e23..c93ea6807da 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt
@@ -1,24 +1,24 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.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 ForgetRoom {
suspend operator fun invoke(roomId: RoomId): Result
}
@ContributesBinding(SessionScope::class)
-class DefaultForgetRoom @Inject constructor(private val client: MatrixClient) : ForgetRoom {
+class DefaultForgetRoom(private val client: MatrixClient) : ForgetRoom {
override suspend fun invoke(roomId: RoomId): Result {
return client.getRoom(roomId)?.use { it.forget() }
?: Result.failure(IllegalStateException("Room not found"))
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
index 5e85c0abbc2..3304169b1ac 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
@@ -1,15 +1,16 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Module
-import dagger.Provides
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.Provides
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@@ -24,7 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import java.util.Optional
-@Module
+@BindingContainer
@ContributesTo(SessionScope::class)
object JoinRoomModule {
@Provides
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt
index 471ef51a534..1e34fddacd2 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/KnockRoom.kt
@@ -1,17 +1,17 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
-import javax.inject.Inject
interface KnockRoom {
suspend operator fun invoke(
@@ -22,7 +22,7 @@ interface KnockRoom {
}
@ContributesBinding(SessionScope::class)
-class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
+class DefaultKnockRoom(private val client: MatrixClient) : KnockRoom {
override suspend fun invoke(
roomIdOrAlias: RoomIdOrAlias,
message: String,
diff --git a/features/joinroom/impl/src/main/res/values-be/translations.xml b/features/joinroom/impl/src/main/res/values-be/translations.xml
index 056a4394f7a..1986a761b0c 100644
--- a/features/joinroom/impl/src/main/res/values-be/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-be/translations.xml
@@ -1,6 +1,6 @@
- "Далучыцца да пакоя"
+ "Далучыцца""Націсніце, каб далучыцца""%1$s пакуль не падтрымлівае прасторы. Вы можаце атрымаць доступ да прастор праз вэб-старонку.""Прасторы пакуль не падтрымліваюцца"
diff --git a/features/joinroom/impl/src/main/res/values-bg/translations.xml b/features/joinroom/impl/src/main/res/values-bg/translations.xml
index e9f7c38fcf8..b395310bdd3 100644
--- a/features/joinroom/impl/src/main/res/values-bg/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-bg/translations.xml
@@ -1,4 +1,5 @@
- "Присъединяване към стаята"
+ "Отхвърляне и блокиране"
+ "Присъединяване"
diff --git a/features/joinroom/impl/src/main/res/values-cs/translations.xml b/features/joinroom/impl/src/main/res/values-cs/translations.xml
index bab8e1f01e3..85403339e84 100644
--- a/features/joinroom/impl/src/main/res/values-cs/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-cs/translations.xml
@@ -1,7 +1,7 @@
- "Z této místnosti jste byl vykázán uživatelem %1$s."
- "Byl vám zakázán vstup do této místnosti"
+ "Byli jste vykázáni uživatelem %1$s."
+ "Byl vám zakázán vstup""Důvod: %1$s.""Zrušit žádost""Ano, zrušit"
@@ -11,13 +11,15 @@
"Opravdu chcete odmítnout pozvánku do této místnosti? Tím také zabráníte tomu, aby vás %1$s kontaktoval(a) nebo pozval(a) do místností.""Odmítnout pozvání a zablokovat""Odmítnout a zablokovat"
- "Vstup do místnosti se nezdařil."
- "Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru."
- "Zapomenout na tuto místnost"
- "Abyste se mohli připojit k této místnosti, potřebujete pozvánku."
- "Připojit se do místnosti"
+ "Vstup se nezdařil"
+ "Buď musíte být pozváni ke vstupu, nebo mohou existovat omezení přístupu."
+ "Zapomenout"
+ "Pro vstup potřebujete pozvánku"
+ "Pozván(a)"
+ "Vstoupit""Abyste se mohli připojit, musíte být pozváni nebo být členem některého prostoru.""Zaklepejte a připojte se"
+ "Povolené znaky %1$d z %2$d""Zpráva (nepovinné)""Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti.""Žádost o vstup odeslána"
diff --git a/features/joinroom/impl/src/main/res/values-cy/translations.xml b/features/joinroom/impl/src/main/res/values-cy/translations.xml
index b093d942205..8b5e3c2d6af 100644
--- a/features/joinroom/impl/src/main/res/values-cy/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-cy/translations.xml
@@ -15,9 +15,11 @@
"Mae\'r ystafell hon naill ai drwy wahoddiad yn unig neu efallai y bydd cyfyngiadau ar fynediad ar lefel y gofod.""Anghofiwch yr ystafell hon""Mae angen gwahoddiad arnoch chi er mwyn ymuno â\'r ystafell hon"
- "Ymuno â\'r ystafell"
+ "Gwahoddwyd gan"
+ "Ymuno""Efallai y bydd angen i chi gael eich gwahodd neu fod yn aelod o ofod er mwyn ymuno.""Anfon cais i ymuno"
+ "Nodau a ganiateir %1$d o %2$d""Neges (dewisol)""Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn.""Anfonwyd y cais i ymuno"
diff --git a/features/joinroom/impl/src/main/res/values-da/translations.xml b/features/joinroom/impl/src/main/res/values-da/translations.xml
index c27ff741455..12613b5878c 100644
--- a/features/joinroom/impl/src/main/res/values-da/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-da/translations.xml
@@ -1,7 +1,7 @@
- "Du blev spærret fra dette rum af %1$s."
- "Du blev spærret fra dette rum"
+ "Du blev spærret af %1$s."
+ "Du blev spærret""Årsag: %1$s.""Annuller anmodning""Ja, annullér"
@@ -11,20 +11,22 @@
"Er du sikker på, at du vil afvise invitationen til at deltage i dette rum? Dette forhindrer også %1$s i at kontakte dig eller invitere dig til andre rum.""Afvis invitation og blokér""Afvis og blokér"
- "Deltagelse i rummet fejlede."
- "Dette rum er enten kun for gæster, eller der kan være sat begrænsninger for adgangen på klyngeniveau."
- "Glem dette rum"
- "Du har brug for en invitation for at deltage i dette rum"
- "Deltag i rummet"
- "Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."
+ "Deltagelse fejlede."
+ "Du skal enten inviteres til at deltage, eller der kan være adgangsbegrænsninger."
+ "Glem"
+ "Du har brug for en invitation for at deltage"
+ "Inviteret af"
+ "Deltag"
+ "Du skal muligvis være inviteret eller være medlem af en gruppe for at deltage.""Send anmodning om at deltage"
+ "Tilladte tegn %1$d af %2$d""Besked (valgfrit)""Du vil modtage en invitation til at deltage i rummet, hvis din anmodning accepteres.""Anmodning om at deltage sendt""Vi kunne ikke forhåndsvise rummet. Dette kan skyldes netværks- eller serverproblemer.""Vi kunne ikke forhåndsvise rummet"
- "%1$s understøtter ikke klynger endnu. Du kan få adgang til klynger på nettet."
- "Klynger er ikke understøttet endnu"
+ "%1$s understøtter ikke grupper endnu. Du kan få adgang til grupper på nettet."
+ "Grupper er ikke understøttet endnu""Klik på knappen nedenfor, og en rumadministrator vil blive underrettet. Du kan deltage i samtalen, når din anmodning er godkendt.""Du skal være medlem af dette rum for at kunne se meddelelseshistorikken.""Vil du deltage i dette rum?"
diff --git a/features/joinroom/impl/src/main/res/values-de/translations.xml b/features/joinroom/impl/src/main/res/values-de/translations.xml
index 0fa2bdce5ff..c6d97d6fcbb 100644
--- a/features/joinroom/impl/src/main/res/values-de/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-de/translations.xml
@@ -1,32 +1,34 @@
- "Sie wurden von %1$s für diesen Chatroom gesperrt."
- "Sie wurden für diesen Chatroom gesperrt"
+ "Du wurdest von %1$s gesperrt."
+ "Du wurdest gesperrt""Grund:%1$s.""Anfrage abbrechen""Ja, abbrechen"
- "Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"
- "Beitrittsanfrage stornieren"
+ "Willst du wirklich deine Anfrage zum Beitritt zu diesem Chat abbrechen?"
+ "Beitrittsanfrage abbrechen""Ja, ablehnen & blockieren"
- "Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."
+ "Bist du sicher, dass du die Einladung zu diesem Chat ablehnen möchtest? Dadurch wird auch jede weitere Kontaktaufnahme oder Chat Einladung von %1$s blockiert.""Einladung ablehnen & Nutzer blockieren""Ablehnen und blockieren"
- "Der Beitritt zum Chatroom schlug fehl."
- "Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces."
- "Vergessen Sie diesen Raum"
- "Sie benötigen eine Einladung, um diesem Chatroom zu betreten"
- "Raum beitreten"
- "Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können."
+ "Beitritt fehlgeschlagen"
+ "Du musst entweder eingeladen werden, um beizutreten, oder es gibt möglicherweise Zugriffsbeschränkungen."
+ "Vergessen"
+ "Du benötigst eine Einladung, um beizutreten"
+ "Eingeladen von"
+ "Beitreten"
+ "Möglicherweise musst du eingeladen werden oder ein Mitglied eines Spaces sein, um beitreten zu können.""Anklopfen"
+ "%1$d von %2$d erlaubte Zeichen""Nachricht (optional)"
- "Falls Ihre Anfrage, den Raum zu betreten, akzeptiert wird, erhalten Sie eine Einladung."
+ "Sollte deine Anfrage akzeptiert werden, erhältst du eine Einladung, dem Chat beizutreten.""Beitrittsanfrage geschickt"
- "Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."
- "Wir konnten diese Chatroomvorschau nicht anzeigen"
- "%1$s unterstützt noch keine Spaces. Sie können auf Spaces im Web zugreifen."
+ "Wir konnten die Chat Vorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."
+ "Wir konnten diese Chat-Vorschau nicht anzeigen"
+ "%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen.""Spaces werden noch nicht unterstützt"
- "Klicken Sie auf die Schaltfläche unten und ein Chatroomadministrator wird benachrichtigt. Nach der Freigabe durch einen Chatroomadministrator können Sie sich an der Unterhaltung beteiligen."
- "Sie müssen Mitglied in diesem Chatroom sein, um den Nachrichtenverlauf einsehen zu können."
- "Möchten Sie diesem Chatroom betreten?"
+ "Klopfe an um einen Admin zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."
+ "Du musst Mitglied in diesem Chat sein, um den Nachrichtenverlauf zu sehen."
+ "Willst du diesem Chat beitreten?""Vorschau nicht verfügbar"
diff --git a/features/joinroom/impl/src/main/res/values-el/translations.xml b/features/joinroom/impl/src/main/res/values-el/translations.xml
index 9a7296b93b3..b12bbb93746 100644
--- a/features/joinroom/impl/src/main/res/values-el/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-el/translations.xml
@@ -15,7 +15,7 @@
"Αυτή η αίθουσα είναι είτε μόνο για προσκεκλημένους είτε ενδέχεται να υπάρχουν περιορισμοί πρόσβασης σε επίπεδο χώρου.""Ξεχάστε αυτή την αίθουσα""Χρειάζεστε πρόσκληση για να συμμετάσχετε σε αυτή την αίθουσα"
- "Συμμετοχή στην αίθουσα"
+ "Συμμετοχή""Ενδέχεται να χρειαστεί να προσκληθείτε ή να είστε μέλος ενός χώρου για να συμμετάσχετε.""Χτύπα για συμμετοχή""Μήνυμα (προαιρετικό)"
diff --git a/features/joinroom/impl/src/main/res/values-es/translations.xml b/features/joinroom/impl/src/main/res/values-es/translations.xml
index b98536b1be4..1f905bbbf7c 100644
--- a/features/joinroom/impl/src/main/res/values-es/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-es/translations.xml
@@ -15,7 +15,7 @@
"O bien solo se puede acceder a esta sala con invitación, o puede que haya restricciones de acceso a nivel de espacio.""Olvidar esta sala""Necesitas una invitación para unirte a esta sala"
- "Unirse a la sala"
+ "Unirse""Es posible que necesites ser invitado o ser miembro de un espacio para poder unirte.""Enviar solicitud de unión""Mensaje (opcional)"
diff --git a/features/joinroom/impl/src/main/res/values-et/translations.xml b/features/joinroom/impl/src/main/res/values-et/translations.xml
index 00ae5f18bbc..572e499d99f 100644
--- a/features/joinroom/impl/src/main/res/values-et/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-et/translations.xml
@@ -11,13 +11,15 @@
"Kas sa oled kindel, et soovid keelduda kutsest sellesse jututuppa? Samaga kaob kasutajal %1$s võimalus sinuga suhelda ja saata sulle jututubade kutseid.""Keeldu kutsest ja blokeeri""Keeldu ja blokeeri"
- "Jututoaga liitumine ei õnnestunud."
- "Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."
+ "Jututoaga liitumine ei õnnestunud"
+ "Ligipääs siia on võimalik vaid kutse alusel või siin kehtivad ligipääsupiirangud.""Unusta see jututuba""Selle jututoaga liitumiseks vajad sa kutset"
- "Liitu jututoaga"
+ "Kutsuja"
+ "Liitu""Selle jututoaga liitumiseks sa vajad kutset või pead juba olema kogukonna liige.""Liitumiseks koputa jututoa uksele"
+ "Lubatud tähemärke: %1$d / %2$d""Selgitus (kui soovid lisada)""Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks.""Liitumispalve on saadetud"
diff --git a/features/joinroom/impl/src/main/res/values-eu/translations.xml b/features/joinroom/impl/src/main/res/values-eu/translations.xml
index 4d6ddd9dbd0..2fe6a761041 100644
--- a/features/joinroom/impl/src/main/res/values-eu/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-eu/translations.xml
@@ -3,9 +3,11 @@
"Arrazoia: %1$s.""Utzi eskaera bertan behera""Bai, utzi bertan behera"
+ "Eman gonbidapenari ezetza eta blokeatu"
+ "Baztertu eta blokeatu""Gelara sartzeak huts egin du.""Ahaztu gela hau"
- "Sartu gelan"
+ "Elkartu""Bidali batzeko eskaera""Mezua (aukerakoa)""Sartzeko eskaera bidali da"
diff --git a/features/joinroom/impl/src/main/res/values-fa/translations.xml b/features/joinroom/impl/src/main/res/values-fa/translations.xml
index 529177cedbf..3f9fc99877c 100644
--- a/features/joinroom/impl/src/main/res/values-fa/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fa/translations.xml
@@ -9,10 +9,12 @@
"بله. رد و انسداد""رد دعوت و انسداد""رد و انسداد"
- "پیوستن به اتاق شکست خورد."
+ "پیوستن شکست خورد""فراموشی این اتاق""برای پیوستن به این اتاق نیاز به دعوت دارید"
- "پیوستن به اتاق"
+ "دعوت شده از سوی"
+ "پیوستن"
+ "برای پیوستن به فضا باید دعوت شده باشید.""در زدن برای پیوستن""پیام (اختیاری)""درخواست پیوستن فرستاده شد"
diff --git a/features/joinroom/impl/src/main/res/values-fi/translations.xml b/features/joinroom/impl/src/main/res/values-fi/translations.xml
index 2a25587398a..38ba043955c 100644
--- a/features/joinroom/impl/src/main/res/values-fi/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fi/translations.xml
@@ -1,7 +1,7 @@
- "%1$s antoi sinulle porttikiellon tästä huoneesta."
- "Sinulle on annettu porttikielto tästä huoneesta"
+ "%1$s antoi sinulle porttikiellon."
+ "Sinulle on annettu porttikielto""Syy: %1$s.""Peruuta pyyntö""Kyllä, peruuta"
@@ -11,13 +11,15 @@
"Oletko varma, että haluat kieltäytyä kutsusta liittyä tähän huoneeseen? Tämä estää myös käyttäjää %1$s ottamasta sinuun yhteyttä tai kutsumasta sinua huoneisiin.""Hylkää kutsu ja estä""Hylkää ja estä"
- "Huoneeseen liittyminen epäonnistui."
- "Tämä huone on tarkoitettu vain kutsutuille, tai siihen saattaa liittyä rajoituksia tilatasolla."
- "Unohda tämä huone"
- "Tarvitset kutsun liittyäksesi tähän huoneeseen"
- "Liity huoneeseen"
+ "Liittyminen epäonnistui"
+ "Sinun on joko saatava kutsu liittyäksesi tai pääsyyn voi olla rajoituksia."
+ "Unohda"
+ "Tarvitset kutsun liittyäksesi"
+ "Kutsuja"
+ "Liity""Saatat tarvita kutsun tai olla tilan jäsen, jotta voit liittyä.""Lähetä liittymispyyntö"
+ "%1$d merkkiä käytetty, %2$d merkkiä sallittu""Viesti (valinnainen)""Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään.""Liittymispyyntö lähetetty"
diff --git a/features/joinroom/impl/src/main/res/values-fr/translations.xml b/features/joinroom/impl/src/main/res/values-fr/translations.xml
index 0652b0c16d4..7ad0317c358 100644
--- a/features/joinroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fr/translations.xml
@@ -1,7 +1,7 @@
- "Vous avez été banni de ce salon par %1$s."
- "Vous avez été banni de ce salon"
+ "Vous avez été banni(e) par %1$s."
+ "Vous avez été banni(e)""Motif: %1$s.""Annuler la demande""Oui, annuler"
@@ -11,20 +11,22 @@
"Êtes-vous sûr de vouloir refuser l’invitation à rejoindre ce salon ? Cela empêchera également %1$s de vous contacter ou de vous inviter dans les salons.""Refuser l’invitation et bloquer""Refuser et bloquer"
- "Rejoindre le salon a échoué."
- "Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau du Space."
- "Oublier ce salon"
- "Vous avez besoin d’une invitation pour rejoindre ce salon"
+ "L’opération a échoué."
+ "Soit vous devez être invité(e) pour rejoindre, soit il peut y avoir des restrictions d’accès."
+ "Oublier"
+ "Vous avez besoin d’une invitation pour pouvoir rejoindre"
+ "Invité(e) par""Rejoindre"
- "Il est possible que vous deviez être invité ou être membre d’un Space pour pouvoir rejoindre le salon."
+ "Il est possible que vous deviez être invité ou être membre d’un Espace pour pouvoir rejoindre le salon.""Demander à joindre"
+ "Caractères autorisés %1$d sur %2$d""Message (facultatif)""Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée.""Demande de rejoindre le salon envoyée""Impossible d’afficher l’aperçu du salon. Cela peut être dû à des problèmes de réseau ou de serveur.""Impossible d’afficher l’aperçu de ce salon"
- "Les Spaces ne sont pas encore pris en charge par %1$s. Vous pouvez voir les Spaces sur le Web."
- "Les Spaces ne sont pas encore pris en charge"
+ "Les Espaces ne sont pas encore pris en charge par %1$s. Vous pouvez voir les Espaces sur le Web."
+ "Les Espaces ne sont pas encore pris en charge""Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion.""Vous devez être un membre du salon pour pouvoir lire l’historique des messages.""Vous souhaitez rejoindre ce salon ?"
diff --git a/features/joinroom/impl/src/main/res/values-hu/translations.xml b/features/joinroom/impl/src/main/res/values-hu/translations.xml
index e6b461abee6..facb3edbf20 100644
--- a/features/joinroom/impl/src/main/res/values-hu/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-hu/translations.xml
@@ -1,7 +1,7 @@
"%1$s kitiltotta a szobából."
- "Kitiltották ebből a szobából"
+ "Kitiltották""Ok: %1$s.""Kérés visszavonása""Igen, visszavonás"
@@ -10,14 +10,16 @@
"Igen, elutasítás és blokkolás""Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja.""Meghívó elutasítása és blokkolás"
- "Elutasítás és blokkolás"
- "A szobához való csatlakozás sikertelen."
- "Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."
- "Szoba elfelejtése"
- "Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához"
- "Csatlakozás a szobához"
+ "Elutasítás és letiltás"
+ "A csatlakozás sikertelen"
+ "Csatlakozáshoz meghívóra van szükség, vagy lehet, hogy korlátozva van a hozzáférés."
+ "Elfelejt"
+ "A csatlakozáshoz meghívóra van szükség."
+ "Meghívta:"
+ "Csatlakozás""A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége.""Kopogtasson a csatlakozáshoz"
+ "Engedélyezett karakterek: %1$d / %2$d""Üzenet (nem kötelező)""Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz.""Csatlakozási kérés elküldve"
diff --git a/features/joinroom/impl/src/main/res/values-in/translations.xml b/features/joinroom/impl/src/main/res/values-in/translations.xml
index 16c078c6ff8..59e3aba2591 100644
--- a/features/joinroom/impl/src/main/res/values-in/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-in/translations.xml
@@ -15,7 +15,7 @@
"Ruangan ini hanya untuk undangan atau mungkin ada pembatasan akses pada tingkat space.""Lupakan ruangan ini""Anda memerlukan undangan untuk bergabung dalam ruangan ini"
- "Gabung dengan ruangan"
+ "Gabung""Anda mungkin perlu diundang atau menjadi anggota space untuk bergabung.""Ketuk untuk bergabung""Pesan (opsional)"
diff --git a/features/joinroom/impl/src/main/res/values-it/translations.xml b/features/joinroom/impl/src/main/res/values-it/translations.xml
index 753b54159a0..deb025db271 100644
--- a/features/joinroom/impl/src/main/res/values-it/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-it/translations.xml
@@ -15,9 +15,10 @@
"Questa stanza è solo su invito o potrebbero esserci delle restrizioni all\'accesso al livello dello spazio.""Dimentica questa stanza""Hai bisogno di un invito per entrare in questa stanza"
- "Entra nella stanza"
+ "Entra""Potrebbe essere necessario essere invitati o essere membro di uno spazio per partecipare.""Bussa per partecipare"
+ "Caratteri consentiti: %1$d di %2$d""Messaggio (opzionale)""Riceverai un invito a entrare nella stanza se la tua richiesta viene accettata.""Richiesta di accesso inviata"
diff --git a/features/joinroom/impl/src/main/res/values-ka/translations.xml b/features/joinroom/impl/src/main/res/values-ka/translations.xml
new file mode 100644
index 00000000000..038b80722be
--- /dev/null
+++ b/features/joinroom/impl/src/main/res/values-ka/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "გაწევრიანება"
+
diff --git a/features/joinroom/impl/src/main/res/values-ko/translations.xml b/features/joinroom/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..80225ac4022
--- /dev/null
+++ b/features/joinroom/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,33 @@
+
+
+ "%1$s 에 의해 이 방에서 퇴장당했습니다."
+ "당신은 이 방에서 차단되었습니다"
+ "이유: %1$s."
+ "요청 취소"
+ "네, 취소합니다"
+ "이 방에 대한 가입 요청을 정말로 취소하시겠습니까?"
+ "가입 요청 취소"
+ "예, 거부 및 차단"
+ "이 방에 대한 초대 거부를 정말로 확인하시겠습니까? 이 경우 %1$s 에서 귀하에게 연락하거나 방에 초대할 수 없게 됩니다."
+ "초대 거부 및 차단"
+ "거부 및 차단"
+ "방에 참여하는데 실패했습니다."
+ "이 방은 초대 전용이거나 스페이스 수준에서 액세스 제한이 있을 수 있습니다."
+ "이 방 지우기"
+ "이 방에 참여하려면 초대장이 필요합니다."
+ "참가하기"
+ "참여하려면 초대 또는 스페이스의 회원이어야 할 수 있습니다."
+ "가입 요청 보내기"
+ "%2$d의 %1$d 문자가 허용됨"
+ "메시지 (선택 사항)"
+ "요청이 승인되면 방에 참여하기 위한 초대장이 발송됩니다."
+ "가입 요청이 전송되었습니다"
+ "방 미리보기를 표시할 수 없습니다. 네트워크 또는 서버 문제 때문일 수 있습니다."
+ "이 방 미리보기를 표시할 수 없습니다."
+ "%1$s 아직 스페이스를 지원하지 않습니다. 웹에서 스페이스에 접속할 수 있습니다."
+ "아직 스페이스가 지원되지 않습니다."
+ "아래 버튼을 클릭하면 방 관리자에게 알림이 전송됩니다. 승인이 완료되면 대화에 참여할 수 있습니다."
+ "이 방의 회원이어야만 메시지 기록을 볼 수 있습니다."
+ "이 방에 참여하고 싶으신가요?"
+ "미리보기 기능은 제공되지 않습니다."
+
diff --git a/features/joinroom/impl/src/main/res/values-nb/translations.xml b/features/joinroom/impl/src/main/res/values-nb/translations.xml
index f1f4d7afda7..5c4873bc57f 100644
--- a/features/joinroom/impl/src/main/res/values-nb/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-nb/translations.xml
@@ -1,7 +1,7 @@
- "Du ble utestengt fra dette rommet av %1$s."
- "Du ble utestengt fra dette rommet"
+ "Du ble utestengt av %1$s."
+ "Du ble utestengt""Årsak: %1$s.""Avbryt forespørsel""Ja, avbryt"
@@ -11,13 +11,15 @@
"Er du sikker på at du vil avslå invitasjonen til å bli med i dette rommet? Dette vil også forhindre %1$s fra å kontakte deg eller invitere deg til rom.""Avslå invitasjon og blokker""Avslå og blokker"
- "Å bli med i rommet mislyktes."
- "Dette rommet er enten kun for inviterte, eller det kan være begrensninger for tilgang på områdenivå."
- "Glem dette rommet"
- "Du trenger en invitasjon for å bli med i dette rommet"
- "Bli med i rommet"
+ "Kunne ikke bli med"
+ "Du må enten bli invitert til å bli med, eller så kan det være begrensninger på tilgangen."
+ "Glem"
+ "Du trenger en invitasjon for å bli med"
+ "Invitert av"
+ "Bli med""Du må kanskje bli invitert eller være medlem av et område for å bli med.""Send forespørsel om å bli med"
+ "Tillatte tegn %1$d av %2$d""Melding (valgfritt)""Du vil motta en invitasjon til å bli med i rommet hvis forespørselen din blir akseptert.""Forespørsel om å bli med sendt"
diff --git a/features/joinroom/impl/src/main/res/values-nl/translations.xml b/features/joinroom/impl/src/main/res/values-nl/translations.xml
index f7d19d9fd1a..36d480b2600 100644
--- a/features/joinroom/impl/src/main/res/values-nl/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-nl/translations.xml
@@ -1,7 +1,10 @@
+ "Reden: %1$s.""Verzoek annuleren"
- "Toetreden tot de kamer"
+ "Weigeren en blokkeren"
+ "Deze kamer vergeten"
+ "Deelnemen""Klop om deel te nemen""Bericht (optioneel)""Je ontvangt een uitnodiging om deel te nemen aan de kamer als je aanvraag wordt geaccepteerd."
diff --git a/features/joinroom/impl/src/main/res/values-pl/translations.xml b/features/joinroom/impl/src/main/res/values-pl/translations.xml
index 56f16fc4757..fcac55d5465 100644
--- a/features/joinroom/impl/src/main/res/values-pl/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-pl/translations.xml
@@ -1,7 +1,7 @@
- "Zostałeś zbanowany z tego pokoju przez %1$s."
- "Zostałeś zbanowany z tego pokoju"
+ "Zostałeś zbanowany przez %1$s ."
+ "Zostałeś zbanowany""Powód: %1$s.""Anuluj prośbę""Tak, anuluj"
@@ -11,13 +11,15 @@
"Czy na pewno chcesz odrzucić zaproszenie dołączenia do tego pokoju? %1$s nie będzie mógł się również z Tobą skontaktować, ani zaprosić Cię do pokoju.""Odrzuć zaproszenie i zablokuj""Odrzuć i zablokuj"
- "Nie udało się dołączyć do pokoju."
- "Ten pokój wymaga zaproszenia lub jest ograniczony z poziomu przestrzeni."
- "Zapomnij o tym pokoju"
- "Potrzebujesz zaproszenia, aby dołączyć do tego pokoju"
- "Dołącz do pokoju"
+ "Nie udało się dołączyć do pokoju"
+ "Ten pokój wymaga zaproszenia lub dołączanie zostało ograniczone."
+ "Zapomnij"
+ "Aby dołączyć, potrzebujesz zaproszenia"
+ "Zaproszony przez"
+ "Dołącz""Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni.""Wyślij prośbę o dołączenie"
+ "Dozwolone znaki %1$d z %2$d""Wiadomość (opcjonalne)""Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana.""Wysłano prośbę o dołączenie"
diff --git a/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml
index 015e235c0ea..ed553bd93a6 100644
--- a/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml
@@ -4,27 +4,28 @@
"Você foi banido desta sala""Motivo: %1$s.""Cancelar pedido"
- "Sim, cancele"
+ "Sim, cancelar""Tem a certeza de que pretende cancelar o seu pedido de adesão a esta sala?"
- "Cancelar pedido de adesão"
+ "Cancelar pedido de entrada""Sim, recusar e bloquear"
- "Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."
+ "Tem certeza de que quer recusar o convite para entrar nesta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para salas.""Recusar convite e bloquear""Recusar e bloquear""A entrada na sala falhou."
- "Esta sala é apenas para convidados ou pode haver restrições de acesso no nível do espaço."
+ "Esta sala é apenas para convidados ou pode haver restrições de acesso a nível do espaço.""Esquecer esta sala""Você precisa de um convite para entrar nesta sala"
- "Entrar na sala"
+ "Entrar""Talvez você precise ser convidado ou ser membro de um espaço para participar."
- "Enviar solicitação para participar"
+ "Enviar solicitação para entrar"
+ "%1$d de %2$d caráteres permitidos""Mensagem (opcional)"
- "Você receberá um convite para participar da sala se seu pedido for aceito."
- "Pedido de adesão enviado"
- "Não foi possível exibir a visualização da sala. Isso pode ser devido a problemas de rede ou de servidor."
- "Não foi possível exibir a visualização desta sala"
+ "Você receberá um convite para entrar nesta sala se seu pedido for aceito."
+ "Pedido de entrada enviado"
+ "Não foi possível exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou do servidor."
+ "Não foi possível exibir a pré-visualização desta sala""%1$s não suporta espaços ainda. Você pode acessar os espaços na web."
- "Os espaços ainda não são compatíveis"
+ "Ainda não há suporte aos espaços""Clique no botão abaixo e um administrador da sala será notificado. Você poderá participar da conversa assim que for aprovado.""Você deve ser um membro desta sala para visualizar o histórico de mensagens.""Quer entrar nesta sala?"
diff --git a/features/joinroom/impl/src/main/res/values-pt/translations.xml b/features/joinroom/impl/src/main/res/values-pt/translations.xml
index ffca14b009d..5a20d62b8c6 100644
--- a/features/joinroom/impl/src/main/res/values-pt/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-pt/translations.xml
@@ -1,7 +1,7 @@
- "Foste banido desta sala por %1$s."
- "Foste banido desta sala."
+ "Foste banido(a) por %1$s."
+ "Foste banido(a)""Razão: %1$s.""Cancelar pedido""Sim, cancelar"
@@ -11,15 +11,17 @@
"Tens a certeza de que queres recusar o convite para entrar nesta sala? Isto também evitará que %1$s te contacte ou te convide para salas.""Recusar convite & bloquear""Recusar e bloquear"
- "Falha ao entrar na sala."
- "A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."
- "Esquecer esta sala"
- "Precisas de um convite para entrares nesta sala"
- "Entrar na sala"
+ "Falha ao entrar"
+ "A entrada pode estar limitada a convites ou pode haver uma outra limitação de acesso."
+ "Esquecer"
+ "Precisas de um convite para entrares"
+ "Convidado por"
+ "Entrar""Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar.""Bater à porta"
+ "%1$d de %2$d caracteres permitidos""Mensagem (opcional)"
- "Irá receber um convite para participar na sala se seu pedido for aceite."
+ "Irás receber um convite para participar na sala se o pedido for aceite.""Pedido de adesão enviado""Não conseguimos exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou servidor.""Não foi possível exibir a pré-visualização desta sala"
diff --git a/features/joinroom/impl/src/main/res/values-ro/translations.xml b/features/joinroom/impl/src/main/res/values-ro/translations.xml
index fb27555bdfc..8326ce06bfa 100644
--- a/features/joinroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ro/translations.xml
@@ -1,18 +1,34 @@
- "Anulați solicitarea"
+ "Ați fost exclus din această cameră de către %1$s."
+ "Ați fost exclus din această cameră."
+ "Motiv: %1$s."
+ "Anulați cererea""Da, anulați"
- "Sunteți sigur că doriți să anulați solicitarea de a vă alătura acestei camere?"
+ "Sunteți sigur că doriți să anulați cererea de a vă alătura acestei camere?""Anulați cererea de alăturare"
- "Alăturați-vă camerei"
- "Bateți pentru a vă alătura"
+ "Da, refuzați și blocați"
+ "Sunteți sigur că doriți să refuzați invitația de a vă alătura acestei camere? Acest lucru va împiedica, de asemenea, %1$s să vă contacteze sau să vă invite în camere."
+ "Refuzați invitația și blocați"
+ "Refuzați și blocați"
+ "Alăturarea la cameră a eșuat."
+ "Această cameră este fie accesibilă numai pe bază de invitație, fie exista restricții de acces la nivel de spațiu."
+ "Uitați această cameră"
+ "Aveți nevoie de o invitație pentru a vă alătura acestei camere."
+ "Invitat de"
+ "Alăturați-vă"
+ "Este posibil să fie necesar să fiți invitat sau să fiți membru al unui spațiu pentru a vă alătura."
+ "Trimiteți o cerere de alăturare"
+ "Caractere permise %1$d din %2$d""Mesaj (opțional)""Veți primi o invitație de a vă alătura camerei dacă cererea dumneavoastră este acceptată.""Cererea de alăturare a fost trimisă"
+ "Nu am putut afișa previzualizarea camerei. Este posibil ca acest lucru să se datoreze unor probleme de rețea sau de server."
+ "Nu am putut afișa previzualizarea acestei camere.""%1$s nu suporta încă spații. Puteți accesa spațiile pe web.""Spațiile nu sunt încă suportate""Faceți clic pe butonul de mai jos și un administrator de cameră va fi notificat. Veți putea să vă alăturați conversației odată aprobată."
- "Trebuie să fiți membru al acestei camere pentru a vizualiza istoricul mesajelor."
+ "Trebuie să fiți membru al acestei camere pentru a vizualiza mesajele anterioare.""Doriți să vă alăturați acestei camere?""Previzualizare indisponibilă"
diff --git a/features/joinroom/impl/src/main/res/values-ru/translations.xml b/features/joinroom/impl/src/main/res/values-ru/translations.xml
index 58a94019c15..207bc4bfde9 100644
--- a/features/joinroom/impl/src/main/res/values-ru/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ru/translations.xml
@@ -1,7 +1,7 @@
- "Вам был запрещен доступ в комнату %1$s."
- "Вам был запрещен доступ в эту комнату"
+ "Вы были заблокированы в комнате %1$s."
+ "Вы были заблокированы в комнате""Причина: %1$s.""Отменить запрос""Да, отменить"
@@ -12,12 +12,14 @@
"Отклонить приглашение и заблокировать""Отклонить и заблокировать""Не удалось присоединиться к комнате."
- "Доступ в эту комнату возможен только по приглашениям или может быть ограничен на уровне пространства."
+ "Доступ к комнате ограничен. Возможно вам нужно приглашение в комнату""Забыть эту комнату""Вам необходимо приглашение для того, чтобы присоединиться к этой комнате"
- "Присоединиться к комнате"
+ "Приглашен"
+ "Присоединиться""Чтобы присоединиться, вам необходимо приглашение или быть участником сообщества."
- "Постучите, чтобы присоединиться"
+ "Отправить запрос на присоединение"
+ "Разрешенные символы %1$d %2$d""Сообщение (опционально)""Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят.""Запрос на присоединение отправлен"
diff --git a/features/joinroom/impl/src/main/res/values-sk/translations.xml b/features/joinroom/impl/src/main/res/values-sk/translations.xml
index 645c523c618..b6af52ca838 100644
--- a/features/joinroom/impl/src/main/res/values-sk/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-sk/translations.xml
@@ -1,7 +1,7 @@
- "Používateľ %1$s vám zakázal prístup do tejto miestnosti."
- "Bol vám zakázaný vstup do tejto miestnosti"
+ "Používateľ %1$s vám zakázal prístup."
+ "Bol vám zakázaný vstup""Dôvod: %1$s.""Zrušiť žiadosť""Áno, zrušiť"
@@ -11,13 +11,15 @@
"Ste si istí, že chcete odmietnuť pozvanie na vstup do tejto miestnosti? To tiež zabráni tomu, aby vás %1$s kontaktoval/a alebo vás pozval/a do miestností.""Odmietnuť pozvánku a zablokovať""Odmietnuť a zablokovať"
- "Pripojenie do miestnosti zlyhalo."
- "Táto miestnosť je buď len pre pozvaných, alebo môžu existovať obmedzenia na prístup na úrovni priestoru."
- "Zabudnúť túto miestnosť"
- "Potrebujete pozvanie, aby ste sa mohli pripojiť k tejto miestnosti"
- "Pripojiť sa do miestnosti"
+ "Vstup sa nepodaril"
+ "Buď musíte byť pozvaní pripojiť sa, alebo môžu existovať obmedzenia prístupu."
+ "Zabudnúť"
+ "Potrebujete pozvanie, aby ste sa mohli pripojiť"
+ "Pozvaný/á používateľom"
+ "Pripojiť sa""Možno budete musieť byť pozvaní alebo byť členom priestoru, aby ste sa mohli pripojiť.""Zaklopaním sa pripojíte"
+ "Povolené znaky %1$d z %2$d""Správa (voliteľné)""Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti.""Žiadosť o pripojenie bola odoslaná"
diff --git a/features/joinroom/impl/src/main/res/values-sv/translations.xml b/features/joinroom/impl/src/main/res/values-sv/translations.xml
index a6aa79ebd96..11af0da734c 100644
--- a/features/joinroom/impl/src/main/res/values-sv/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-sv/translations.xml
@@ -15,9 +15,10 @@
"Detta rum är antingen endast för inbjudna eller så kan det finnas begränsningar för åtkomst på utrymmesnivå.""Glöm det här rummet""Du behöver en inbjudan för att gå med i detta rum"
- "Gå med i rummet"
+ "Gå med""Du kan behöva bli inbjuden eller vara medlem i ett utrymme för att gå med.""Knacka för att gå med"
+ "Tillåtna tecken %1$d av %2$d""Meddelande (valfritt)""Du kommer att få en inbjudan att gå med i rummet om din begäran accepteras.""Begäran om att gå med skickad"
diff --git a/features/joinroom/impl/src/main/res/values-tr/translations.xml b/features/joinroom/impl/src/main/res/values-tr/translations.xml
index fb3edc84b2c..abd08d3c867 100644
--- a/features/joinroom/impl/src/main/res/values-tr/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-tr/translations.xml
@@ -10,12 +10,11 @@
"Evet, reddet ve engelle""Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller.""Daveti reddet ve engelle"
- "Reddet ve engelle""Odaya katılım başarısız oldu.""Bu odaya yalnızca davetle girilebilir veya alan düzeyinde erişim kısıtlamaları olabilir.""Bu odayı unut""Bu odaya katılmak için bir davete ihtiyacınız var"
- "Odaya katıl"
+ "Katıl""Katılmak için davet edilmeniz veya bir alana üye olmanız gerekebilir.""Katılma isteği gönder""Mesaj (isteğe bağlı)"
diff --git a/features/joinroom/impl/src/main/res/values-uk/translations.xml b/features/joinroom/impl/src/main/res/values-uk/translations.xml
index 72a085042f0..90d5b014417 100644
--- a/features/joinroom/impl/src/main/res/values-uk/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-uk/translations.xml
@@ -15,9 +15,10 @@
"Ця кімната доступна лише за запрошенням або на рівні простору можуть бути обмеження доступу.""Забути цю кімнату""Вам потрібне запрошення, щоб приєднатися до цієї кімнати"
- "Приєднатися до кімнати"
+ "Доєднатися""Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися.""Постукати, щоб приєднатися"
+ "Дозволені символи %1$d з %2$d""Повідомлення (необов\'язково)""Ви отримаєте запрошення приєднатися до кімнати, якщо ваш запит буде прийнятий.""Запит на приєднання надіслано"
diff --git a/features/joinroom/impl/src/main/res/values-ur/translations.xml b/features/joinroom/impl/src/main/res/values-ur/translations.xml
index af34c737107..4bb703073c3 100644
--- a/features/joinroom/impl/src/main/res/values-ur/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-ur/translations.xml
@@ -1,6 +1,6 @@
- "کمرے میں شامل ہوں"
+ "شامل ہوں""شامل ہونے کی درخواست بھیجیں""%1$s ابھی تک خالی جگہوں کی حمایت نہیں کرتا۔ آپ جال پر خالی جگہوں تک رسائی حاصل کرسکتے ہیں۔""ابھی تک جگہیں تعاون یافتہ نہیں"
diff --git a/features/joinroom/impl/src/main/res/values-uz/translations.xml b/features/joinroom/impl/src/main/res/values-uz/translations.xml
new file mode 100644
index 00000000000..e8c50922c41
--- /dev/null
+++ b/features/joinroom/impl/src/main/res/values-uz/translations.xml
@@ -0,0 +1,19 @@
+
+
+ "So‘rovni bekor qilish"
+ "Ha, bekor qiling"
+ "Bu xonaga qo‘shilish so‘rovingizni bekor qilishni xohlayotganingizga ishonchingiz komilmi?"
+ "Qo‘shilish so‘rovini bekor qilish"
+ "Rad etish va bloklash"
+ "Qo\'shilish"
+ "Qoʻshilish soʻrovini yuborish"
+ "Xabar (ixtiyoriy)"
+ "Agar so‘rovingiz qabul qilinsa, xonaga qo‘shilish taklifini olasiz."
+ "Qo‘shilish so‘rovi yuborildi"
+ "%1$s hali maydon xizmatini qoʻllab-quvvatlamaydi. maydonga veb-sayt orqali kirishingiz mumkin."
+ "Maydonlar hali qoʻllab-quvvatlanmaydi"
+ "Quyidagi tugmani bosing va xona administratoriga xabar beriladi. Ruxsat berilgandan soʻng suhbatga qoʻshilishingiz mumkin boʻladi."
+ "Xabarlar tarixini koʻrish uchun siz ushbu xonaning aʼzosi boʻlishingiz shart."
+ "Bu xonaga qoʻshilishni xohlaysizmi?"
+ "Oldindan koʻrish imkoni yoʻq"
+
diff --git a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
index b176a76e0f2..00887ad12a7 100644
--- a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml
@@ -1,7 +1,7 @@
- "您被 %1$s 禁止進入此聊天室。"
- "您被禁止進入此聊天室"
+ "您被 %1$s 禁止。"
+ "您被禁止了""理由:%1$s。""取消請求""是的,取消"
@@ -11,13 +11,15 @@
"您確定要拒絕加入此聊天室的邀請嗎?這也會防止 %1$s 聯絡您或邀請您加入聊天室。""拒絕邀請並封鎖""拒絕並封鎖"
- "加入聊天室失敗。"
- "此聊天室僅有受邀者才能進入,或是在空間層級有存取限制。"
- "忘記此聊天室"
- "您需要獲得邀請才能加入此聊天室"
- "加入聊天室"
+ "加入失敗。"
+ "您必須獲得邀請才能加入,或者可能存在存取限制。"
+ "忘記"
+ "您需要獲得邀請才能加入"
+ "邀請者"
+ "加入""您可能需要被邀請成為空間的成員才能加入。""傳送加入請求"
+ "允許的字元 %1$d 中的 %2$d""訊息(選擇性)""若接受了您的請求,您將會收到加入聊天是的邀請。""已傳送加入請求"
diff --git a/features/joinroom/impl/src/main/res/values-zh/translations.xml b/features/joinroom/impl/src/main/res/values-zh/translations.xml
index 8b6e9088c1b..52361e01aaa 100644
--- a/features/joinroom/impl/src/main/res/values-zh/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml
@@ -7,16 +7,24 @@
"是的,取消""您确定要取消加入此房间的请求吗?""取消加入申请"
+ "是的,拒绝并屏蔽"
+ "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"
+ "拒绝邀请并屏蔽"
+ "拒绝并屏蔽""加入房间失败。""要么此房间仅限受邀者,要么可能在空间层级有加入限制。""忘记这个房间""你需要邀请才能加入这个房间"
- "加入聊天室"
+ "受邀于"
+ "加入""您可能需要受到邀请或成为某个空间的成员才能加入。""加入聊天室"
+ "允许的字符数量 %2$d中的%1$d""消息(可选)""如果您的请求被接受,您将收到加入房间的邀请。""加入请求已发送"
+ "无法显示房间预览。这可能是由于网络或服务器问题造成的。"
+ "无法显示此房间预览""%1$s 尚不支持空间。您可以通过 Web 端访问空间""空间尚不支持""点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。"
diff --git a/features/joinroom/impl/src/main/res/values/localazy.xml b/features/joinroom/impl/src/main/res/values/localazy.xml
index a3e91c542c6..0e1787fc0b0 100644
--- a/features/joinroom/impl/src/main/res/values/localazy.xml
+++ b/features/joinroom/impl/src/main/res/values/localazy.xml
@@ -1,7 +1,7 @@
- "You were banned from this room by %1$s."
- "You were banned from this room"
+ "You were banned by %1$s."
+ "You were banned""Reason: %1$s.""Cancel request""Yes, cancel"
@@ -11,13 +11,15 @@
"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms.""Decline invite & block""Decline and block"
- "Joining the room failed."
- "This room is either invite-only or there might be restrictions to access at space level."
- "Forget this room"
- "You need an invite in order to join this room"
- "Join room"
+ "Joining failed"
+ "You either need to be invited to join or there might be restrictions to access."
+ "Forget"
+ "You need an invite in order to join"
+ "Invited by"
+ "Join""You may need to be invited or be a member of a space in order to join.""Send request to join"
+ "Allowed characters %1$d of %2$d""Message (optional)""You will receive an invite to join the room if your request is accepted.""Request to join sent"
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt
new file mode 100644
index 00000000000..3f44650c3f4
--- /dev/null
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPointTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.joinroom.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import im.vector.app.features.analytics.plan.JoinedRoom
+import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
+import io.element.android.features.joinroom.api.JoinRoomEntryPoint
+import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+import java.util.Optional
+
+class DefaultJoinRoomEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultJoinRoomEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ JoinRoomFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenterFactory = { _, _, _, _, _ -> createJoinRoomPresenter() },
+ acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
+ declineAndBlockEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
+ )
+ }
+ val inputs = JoinRoomEntryPoint.Inputs(
+ roomId = A_ROOM_ID,
+ roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
+ roomDescription = Optional.ofNullable(null),
+ serverNames = emptyList(),
+ trigger = JoinedRoom.Trigger.RoomDirectory,
+ )
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ inputs = inputs,
+ )
+ assertThat(result).isInstanceOf(JoinRoomFlowNode::class.java)
+ assertThat(result.plugins).contains(inputs)
+ }
+}
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt
index e7f487fadeb..8badc1bc2c1 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeCancelKnockRoom.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt
index 48d3eec0776..b7e0fc5b079 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt
index 6ee281c8536..d80312fb621 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeKnockRoom.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
index 8a475668ae1..aec50d17616 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,6 +14,7 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
+import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
@@ -28,13 +30,11 @@ 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.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.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
-import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.test.AN_EXCEPTION
@@ -50,14 +50,19 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomPreview
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
+import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
+import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
+import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.matrix.ui.model.toInviteSender
+import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@@ -89,6 +94,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo()
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -106,7 +114,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.joinedMembersCount)
- assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect)
+ assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = roomInfo.isDirect))
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
}
}
@@ -117,6 +125,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -141,7 +152,7 @@ class JoinRoomPresenterTest {
@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 inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo(
currentUserMembership = CurrentUserMembership.INVITED,
@@ -150,7 +161,73 @@ class JoinRoomPresenterTest {
)
val inviteData = roomInfo.toInviteData()
val matrixClient = FakeMatrixClient(
- getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ getNotJoinedRoomResult = { _, _ ->
+ Result.success(
+ aRoomPreview(
+ info = aRoomPreviewInfo(
+ numberOfJoinedMembers = 5,
+ ),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
+ )
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
+ ).apply {
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.of(roomInfo))
+ }
+ }
+ val presenter = createJoinRoomPresenter(
+ matrixClient = matrixClient
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender))
+ assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5)
+ }
+ }
+ }
+
+ @Test
+ fun `present - when space is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
+ val inviter = aRoomMember(userId = A_USER_ID_2, displayName = "Bob")
+ val expectedInviteSender = inviter.toInviteSender()
+ val spaceHero = aMatrixUser()
+ val roomInfo = aRoomInfo(
+ isSpace = true,
+ currentUserMembership = CurrentUserMembership.INVITED,
+ joinedMembersCount = 5,
+ inviter = inviter,
+ heroes = listOf(spaceHero),
+ )
+ val inviteData = roomInfo.toInviteData()
+ val matrixClient = FakeMatrixClient(
+ getNotJoinedRoomResult = { _, _ ->
+ Result.success(
+ aRoomPreview(
+ info = aRoomPreviewInfo(
+ numberOfJoinedMembers = 5,
+ ),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
+ )
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = {
+ FakeSpaceRoomList(
+ initialSpaceFlowValue = aSpaceRoom(
+ childrenCount = 3,
+ )
+ )
+ },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -164,6 +241,85 @@ class JoinRoomPresenterTest {
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender))
assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5)
+ // Space details are provided
+ assertThat(state.contentState.details).isEqualTo(
+ LoadedDetails.Space(
+ childrenCount = 3,
+ heroes = persistentListOf(spaceHero),
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `present - space is invited - no room info`() = runTest {
+ val spaceHero = aMatrixUser()
+ val spaceRoom = aSpaceRoom(
+ childrenCount = 3,
+ heroes = listOf(spaceHero),
+ )
+ val matrixClient = FakeMatrixClient(
+ getNotJoinedRoomResult = { _, _ ->
+ Result.failure(Exception("Error"))
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = {
+ FakeSpaceRoomList(
+ initialSpaceFlowValue = spaceRoom,
+ )
+ },
+ ),
+ ).apply {
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.ofNullable(null))
+ }
+ }
+ val presenter = createJoinRoomPresenter(
+ matrixClient = matrixClient
+ )
+ presenter.test {
+ skipItems(1)
+ awaitItem().also { state ->
+ // Space details are provided
+ assertThat((state.contentState as ContentState.Loaded).details).isEqualTo(
+ LoadedDetails.Space(
+ childrenCount = 3,
+ heroes = persistentListOf(spaceHero),
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `present - space is invited - no room info - space room state set`() = runTest {
+ val spaceRoom = aSpaceRoom(
+ state = CurrentUserMembership.INVITED,
+ )
+ val matrixClient = FakeMatrixClient(
+ getNotJoinedRoomResult = { _, _ ->
+ Result.failure(Exception("Error"))
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = {
+ FakeSpaceRoomList(
+ initialSpaceFlowValue = spaceRoom,
+ )
+ },
+ ),
+ ).apply {
+ getRoomInfoFlowLambda = { _ ->
+ flowOf(Optional.ofNullable(null))
+ }
+ }
+ val presenter = createJoinRoomPresenter(
+ matrixClient = matrixClient
+ )
+ presenter.test {
+ awaitItem().also { state ->
+ // Space details are provided
+ assertThat(state.contentState).isInstanceOf(ContentState.Loading::class.java)
}
}
}
@@ -181,10 +337,16 @@ class JoinRoomPresenterTest {
aRoomPreview(
info = aRoomPreviewInfo(
numberOfJoinedMembers = 10,
- )
+ ),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
)
)
},
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -208,7 +370,11 @@ class JoinRoomPresenterTest {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
- val matrixClient = FakeMatrixClient().apply {
+ val matrixClient = FakeMatrixClient(
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
+ ).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
}
@@ -243,6 +409,9 @@ class JoinRoomPresenterTest {
}
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@@ -271,6 +440,9 @@ class JoinRoomPresenterTest {
fun `present - when room is joined with error, it is possible to clear the error`() = runTest {
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@@ -333,16 +505,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.BANNED,
),
roomMembershipDetails = {
- Result.success(
- RoomMembershipDetails(
- currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
- senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
- )
- )
+ Result.success(aRoomMembershipDetails())
}
)
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -370,6 +540,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -391,6 +564,9 @@ class JoinRoomPresenterTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
).apply {
getRoomInfoFlowLambda = { _ ->
flowOf(Optional.of(roomInfo))
@@ -422,7 +598,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.topic).isEqualTo(roomDescription.topic)
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
- assertThat(contentState.isDm).isFalse()
+ assertThat(contentState.details).isEqualTo(aLoadedDetailsRoom(isDm = false))
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
}
}
@@ -496,6 +672,9 @@ class JoinRoomPresenterTest {
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@@ -541,6 +720,9 @@ class JoinRoomPresenterTest {
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@@ -585,6 +767,9 @@ class JoinRoomPresenterTest {
val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess)
val matrixClient = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient,
@@ -633,10 +818,16 @@ class JoinRoomPresenterTest {
isHistoryWorldReadable = false,
joinRule = JoinRule.Public,
currentUserMembership = null,
- )
+ ),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
)
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -651,10 +842,10 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
- isDm = false,
- roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
- joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
+ joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin,
+ joinRule = JoinRule.Public,
+ details = aLoadedDetailsRoom(isDm = false),
)
)
}
@@ -680,16 +871,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.INVITED,
),
roomMembershipDetails = {
- Result.success(
- RoomMembershipDetails(
- currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
- senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
- )
- )
+ Result.success(aRoomMembershipDetails())
}
)
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -704,8 +893,6 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
- isDm = false,
- roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
inviteData = InviteData(
@@ -723,7 +910,9 @@ class JoinRoomPresenterTest {
),
membershipChangeReason = null,
),
- )
+ ),
+ joinRule = JoinRule.Public,
+ details = aLoadedDetailsRoom(isDm = false),
)
)
}
@@ -750,15 +939,15 @@ class JoinRoomPresenterTest {
),
roomMembershipDetails = {
Result.success(
- RoomMembershipDetails(
- currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
- senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
- )
+ aRoomMembershipDetails(),
)
}
)
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -773,8 +962,6 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
- isDm = false,
- roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
banSender = InviteSender(
@@ -788,7 +975,9 @@ class JoinRoomPresenterTest {
membershipChangeReason = null,
),
reason = null,
- )
+ ),
+ joinRule = JoinRule.Public,
+ details = aLoadedDetailsRoom(isDm = false),
)
)
}
@@ -814,16 +1003,14 @@ class JoinRoomPresenterTest {
currentUserMembership = CurrentUserMembership.KNOCKED,
),
roomMembershipDetails = {
- Result.success(
- RoomMembershipDetails(
- currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
- senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
- )
- )
+ Result.success(aRoomMembershipDetails())
}
)
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -838,10 +1025,10 @@ class JoinRoomPresenterTest {
topic = "Room topic",
alias = RoomAlias("#alias:matrix.org"),
numberOfMembers = 2,
- isDm = false,
- roomType = RoomType.Room,
roomAvatarUrl = "avatarUrl",
- joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
+ joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked,
+ joinRule = JoinRule.Public,
+ details = aLoadedDetailsRoom(isDm = false),
)
)
}
@@ -853,9 +1040,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Private))
+ aRoomPreview(
+ info = aRoomPreviewInfo(joinRule = JoinRule.Private),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -873,9 +1068,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom")))
+ aRoomPreview(
+ info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom")),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -893,9 +1096,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Invite))
+ aRoomPreview(
+ info = aRoomPreviewInfo(joinRule = JoinRule.Invite),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -913,9 +1124,19 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList())))
+ aRoomPreview(
+ info = aRoomPreviewInfo(
+ joinRule = JoinRule.KnockRestricted(persistentListOf())
+ ),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ }
+ )
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -933,9 +1154,17 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
- aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList())))
+ aRoomPreview(
+ info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf())),
+ roomMembershipDetails = {
+ Result.success(aRoomMembershipDetails())
+ },
+ )
)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -948,62 +1177,15 @@ class JoinRoomPresenterTest {
}
}
- @Test
- fun `present - when room is not known RoomPreview is loaded as Space`() = runTest {
- val client = FakeMatrixClient(
- getNotJoinedRoomResult = { _, _ ->
- Result.success(
- aRoomPreview(info = aRoomPreviewInfo(isSpace = true))
- )
- }
- )
- val presenter = createJoinRoomPresenter(
- matrixClient = client
- )
- presenter.test {
- skipItems(1)
- awaitItem().also { state ->
- assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsSpace("AppName"))
- }
- }
- }
-
@Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION)
- }
- )
- val presenter = createJoinRoomPresenter(
- matrixClient = client
- )
- presenter.test {
- skipItems(1)
- awaitItem().also { state ->
- assertThat(state.contentState).isEqualTo(
- ContentState.Failure(error = AN_EXCEPTION)
- )
- state.eventSink(JoinRoomEvents.RetryFetchingContent)
- }
- skipItems(1)
- awaitItem().also { state ->
- assertThat(state.contentState).isEqualTo(ContentState.Loading)
- }
- awaitItem().also { state ->
- assertThat(state.contentState).isEqualTo(
- ContentState.Failure(error = AN_EXCEPTION)
- )
- }
- }
- }
-
- @Test
- fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest {
- val client = FakeMatrixClient(
- getNotJoinedRoomResult = { _, _ ->
- Result.failure(AN_EXCEPTION)
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -1012,13 +1194,8 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
- ContentState.Failure(error = AN_EXCEPTION)
+ ContentState.UnknownRoom
)
- state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
- }
- skipItems(1)
- awaitItem().also { state ->
- assertThat(state.contentState).isEqualTo(ContentState.Dismissing)
}
}
}
@@ -1028,7 +1205,10 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null))
- }
+ },
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
@@ -1041,39 +1221,6 @@ class JoinRoomPresenterTest {
}
}
- private fun createJoinRoomPresenter(
- roomId: RoomId = A_ROOM_ID,
- roomDescription: Optional = Optional.empty(),
- serverNames: List = emptyList(),
- trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
- matrixClient: MatrixClient = FakeMatrixClient(),
- joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ ->
- Result.success(Unit)
- },
- knockRoom: KnockRoom = FakeKnockRoom(),
- cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
- forgetRoom: ForgetRoom = FakeForgetRoom(),
- buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
- acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() },
- seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
- ): JoinRoomPresenter {
- return JoinRoomPresenter(
- roomId = roomId,
- roomIdOrAlias = roomId.toRoomIdOrAlias(),
- roomDescription = roomDescription,
- serverNames = serverNames,
- trigger = trigger,
- matrixClient = matrixClient,
- joinRoom = FakeJoinRoom(joinRoomLambda),
- knockRoom = knockRoom,
- cancelKnockRoom = cancelKnockRoom,
- forgetRoom = forgetRoom,
- buildMeta = buildMeta,
- acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
- seenInvitesStore = seenInvitesStore,
- )
- }
-
private fun aRoomDescription(
roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
@@ -1094,3 +1241,45 @@ class JoinRoomPresenterTest {
)
}
}
+
+internal fun createJoinRoomPresenter(
+ roomId: RoomId = A_ROOM_ID,
+ roomDescription: Optional = Optional.empty(),
+ serverNames: List = emptyList(),
+ trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
+ matrixClient: MatrixClient = FakeMatrixClient(
+ spaceService = FakeSpaceService(
+ spaceRoomListResult = { FakeSpaceRoomList() },
+ ),
+ ),
+ joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ ->
+ Result.success(Unit)
+ },
+ knockRoom: KnockRoom = FakeKnockRoom(),
+ cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
+ forgetRoom: ForgetRoom = FakeForgetRoom(),
+ buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
+ acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() },
+ seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
+): JoinRoomPresenter {
+ return JoinRoomPresenter(
+ roomId = roomId,
+ roomIdOrAlias = roomId.toRoomIdOrAlias(),
+ roomDescription = roomDescription,
+ serverNames = serverNames,
+ trigger = trigger,
+ matrixClient = matrixClient,
+ joinRoom = FakeJoinRoom(joinRoomLambda),
+ knockRoom = knockRoom,
+ cancelKnockRoom = cancelKnockRoom,
+ forgetRoom = forgetRoom,
+ buildMeta = buildMeta,
+ acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
+ seenInvitesStore = seenInvitesStore,
+ )
+}
+
+private fun aRoomMembershipDetails() = RoomMembershipDetails(
+ currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
+ senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
+)
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
index 0205c1cac0f..0a3b1ca3c68 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,7 +15,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.test.anInviteData
import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.ui.model.toInviteSender
@@ -218,21 +218,6 @@ class JoinRoomViewTest {
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
}
- @Test
- fun `clicking on ok when a space is displayed invokes the expected callback`() {
- val eventsRecorder = EventsRecorder(expectEvents = false)
- ensureCalledOnce {
- rule.setJoinRoomView(
- aJoinRoomState(
- contentState = aLoadedContentState(roomType = RoomType.Space),
- eventSink = eventsRecorder,
- ),
- onBackClick = it
- )
- rule.clickOn(CommonStrings.action_ok)
- }
- }
-
@Test
fun `clicking on ok when user is unauthorized the expected callback`() {
val eventsRecorder = EventsRecorder(expectEvents = false)
diff --git a/features/knockrequests/api/build.gradle.kts b/features/knockrequests/api/build.gradle.kts
index 4beea72bed7..25f419acff1 100644
--- a/features/knockrequests/api/build.gradle.kts
+++ b/features/knockrequests/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt
index 9886c439e1f..34061db3353 100644
--- a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt
+++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt
index b1b174580eb..9975911266d 100644
--- a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt
+++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts
index 39004b30e0e..6f030479f5a 100644
--- a/features/knockrequests/impl/build.gradle.kts
+++ b/features/knockrequests/impl/build.gradle.kts
@@ -1,11 +1,13 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
plugins {
id("io.element.android-compose-library")
@@ -21,7 +23,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.knockrequests.api)
@@ -33,15 +35,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.featureflag.test)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt
deleted file mode 100644
index 992098dc91a..00000000000
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.knockrequests.impl.banner
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-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.AvatarSize
-import io.element.android.libraries.designsystem.components.avatar.AvatarType
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.text.toPx
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toImmutableList
-
-/**
- * Draw a row of avatars (they must all have the same size), from start to end.
- * @param avatarDataList the avatars to render. Note: they will all be rendered, the caller may
- * want to limit the list size
- * @param avatarType the type of avatars to render
- * @param modifier Jetpack Compose modifier
- * @param overlapRatio the overlap ration. When 0f, avatars will render without overlap, when 1f
- * only the first avatar will be visible
- */
-@Composable
-fun AvatarRow(
- avatarDataList: ImmutableList,
- avatarType: AvatarType,
- modifier: Modifier = Modifier,
- overlapRatio: Float = 0.5f,
-) {
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- Box(
- modifier = modifier,
- ) {
- val lastItemIndex = avatarDataList.size - 1
- val avatarSize = avatarDataList.firstOrNull()?.size?.dp ?: return
- val avatarSizePx = avatarSize.toPx()
- avatarDataList
- .reversed()
- .forEachIndexed { index, avatarData ->
- Avatar(
- modifier = Modifier
- .padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index))
- .graphicsLayer {
- compositingStrategy = CompositingStrategy.Offscreen
- }
- .drawWithContent {
- // Draw content and clear the pixels for the avatar on the left (right in RTL).
- drawContent()
- val xOffset = if (isRtl) {
- size.width - avatarSizePx * (overlapRatio - 0.5f)
- } else {
- 0f + avatarSizePx * (overlapRatio - 0.5f)
- }
- if (index < lastItemIndex) {
- drawCircle(
- color = Color.Black,
- center = Offset(
- x = xOffset,
- y = size.height / 2,
- ),
- radius = avatarSizePx / 2,
- blendMode = BlendMode.Clear,
- )
- }
- }
- .size(size = avatarSize)
- // Keep internal padding, it has the advantage to not reduce the size of the Avatar image,
- // which is already small in our use case.
- .padding(2.dp),
- avatarData = avatarData,
- avatarType = avatarType,
- )
- }
- }
-}
-
-@Composable
-@PreviewsDayNight
-internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
- ElementPreview {
- ContentToPreview(overlapRatio)
- }
-}
-
-@Composable
-@PreviewsDayNight
-internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
- CompositionLocalProvider(
- LocalLayoutDirection provides LayoutDirection.Rtl,
- ) {
- ElementPreview {
- ContentToPreview(overlapRatio)
- }
- }
-}
-
-@Composable
-private fun ContentToPreview(overlapRatio: Float) {
- AvatarRow(
- avatarDataList = listOf("A", "B", "C").map {
- AvatarData(
- id = it,
- name = it,
- size = AvatarSize.RoomListItem,
- )
- }.toImmutableList(),
- avatarType = AvatarType.User,
- overlapRatio = overlapRatio,
- )
-}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt
index 994ba025a0f..32e551a09d8 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,13 +10,12 @@ package io.element.android.features.knockrequests.impl.banner
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
import io.element.android.libraries.di.RoomScope
-import javax.inject.Inject
@ContributesBinding(RoomScope::class)
-class DefaultKnockRequestsBannerRenderer @Inject constructor(
+class DefaultKnockRequestsBannerRenderer(
private val presenter: KnockRequestsBannerPresenter,
) : KnockRequestsBannerRenderer {
@Composable
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
index 2100e1f5ffa..740982b612c 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
index 737a3b0562c..641efffaa32 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,6 +15,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import dev.zacsweers.metro.Inject
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.architecture.Presenter
@@ -23,11 +25,11 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import javax.inject.Inject
private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L
-class KnockRequestsBannerPresenter @Inject constructor(
+@Inject
+class KnockRequestsBannerPresenter(
private val knockRequestsService: KnockRequestsService,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
@@ -51,7 +53,7 @@ class KnockRequestsBannerPresenter @Inject constructor(
}
}
- fun handleEvents(event: KnockRequestsBannerEvents) {
+ fun handleEvent(event: KnockRequestsBannerEvents) {
when (event) {
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
sessionCoroutineScope.acceptSingleKnockRequest(
@@ -72,7 +74,7 @@ class KnockRequestsBannerPresenter @Inject constructor(
displayAcceptError = showAcceptError.value,
canAccept = permissions.canAccept,
isVisible = shouldShowBanner,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
index 1912891afab..e9291b501ad 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
index 3cdbeba9b28..67f1aaae8fe 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
index 334bb531ae9..431bd14d105 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -38,6 +39,7 @@ import io.element.android.libraries.designsystem.components.async.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarRow
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt
index 81603d6a680..17fd0ec64a1 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt
index 4de596a623e..2ca4d4df749 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt
index 1e2e549db69..dc6aeb8f0d4 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt
index de3bcd68721..15af1b2fc51 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt
index 71cab7134ea..059b1284824 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
index 671208e97b3..eeba54f87d7 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
@@ -1,22 +1,23 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.knockrequests.impl.data
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Module
-import dagger.Provides
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.Provides
+import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.RoomScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom
-@Module
+@BindingContainer
@ContributesTo(RoomScope::class)
object KnockRequestsModule {
@Provides
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt
index ccfc0b1b1fa..04c2f7b316d 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt
index e5181be27c3..74a8b712d3d 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,14 +10,13 @@ package io.element.android.features.knockrequests.impl.list
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultKnockRequestsListEntryPoint @Inject constructor() : KnockRequestsListEntryPoint {
+class DefaultKnockRequestsListEntryPoint : KnockRequestsListEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode(buildContext)
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
index c00ffdaa502..f1350b16697 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt
index 52cc5eb9288..f3c8b068843 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,13 +13,14 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class)
-class KnockRequestsListNode @AssistedInject constructor(
+@AssistedInject
+class KnockRequestsListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: KnockRequestsListPresenter,
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
index aa537384b92..a56dc407311 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -16,15 +17,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class KnockRequestsListPresenter @Inject constructor(
+@Inject
+class KnockRequestsListPresenter(
private val knockRequestsService: KnockRequestsService,
) : Presenter {
@Composable
@@ -37,7 +39,7 @@ class KnockRequestsListPresenter @Inject constructor(
val coroutineScope = rememberCoroutineScope()
- fun handleEvents(event: KnockRequestsListEvents) {
+ fun handleEvent(event: KnockRequestsListEvents) {
when (event) {
KnockRequestsListEvents.AcceptAll -> {
currentAction = KnockRequestsAction.AcceptAll
@@ -72,7 +74,7 @@ class KnockRequestsListPresenter @Inject constructor(
currentAction = currentAction,
permissions = permissions,
asyncAction = asyncAction.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
index 1042a3646fa..a1bb90cae89 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
index a83450e9bde..2c4c92a6b68 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
index 987125f451c..1ccc0742d65 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/main/res/values-da/translations.xml b/features/knockrequests/impl/src/main/res/values-da/translations.xml
index 30ac6a8d306..795cda658cb 100644
--- a/features/knockrequests/impl/src/main/res/values-da/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-da/translations.xml
@@ -32,5 +32,5 @@
"Se alle""Accepter""%1$s ønsker at deltage i dette rum"
- "Se"
+ "Vis"
diff --git a/features/knockrequests/impl/src/main/res/values-de/translations.xml b/features/knockrequests/impl/src/main/res/values-de/translations.xml
index 45df2ecc8f3..33e9e1be704 100644
--- a/features/knockrequests/impl/src/main/res/values-de/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-de/translations.xml
@@ -1,36 +1,36 @@
"Ja, akzeptiere alle"
- "Sind Sie sicher, dass Sie alle Beitrittsanfragen akzeptieren möchten?"
- "Akzeptiere alle Anfragen"
+ "Bist du sicher, dass du alle Beitrittsanfragen akzeptieren möchtest?"
+ "Akzeptiere alle Beitrittsanfragen""Alle akzeptieren"
- "Wir konnten nicht alle Anfragen annehmen. Möchten Sie es noch einmal versuchen?"
- "Es konnten nicht alle Anfragen akzeptiert werden"
+ "Wir konnten nicht alle Beitrittsanfragen annehmen. Möchtest du es noch mal versuchen?"
+ "Es konnten nicht alle Beitrittsanfragen akzeptiert werden""Alle Beitrittsanfragen werden angenommen"
- "Wir konnten diese Anfrage nicht annehmen. Möchten Sie es noch einmal versuchen?"
- "Die Anfrage konnte nicht akzeptiert werden"
+ "Wir konnten diese Beitrittsanfrage nicht annehmen. Möchtest du es noch mal versuchen?"
+ "Die Beitrittsanfrage konnte nicht akzeptiert werden""Beitrittsanfrage annehmen""Ja, ablehnen und sperren"
- "Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten?Dieser Benutzer kann keine erneute Zulassung auf diesen Chatroom anfordern."
- "Ablehnen und Zugriff verbieten"
+ "Bist du sicher, dass du %1$s ablehnen und sperren möchtest? Dieser Nutzer kann dann keinen erneuten Beitritt zu diesem Chat anfragen."
+ "Ablehnen und Zugriff sperren""Ablehnung und Sperrung des Zugriffs""Ja, ablehnen"
- "Sind Sie sicher, dass Sie die %1$s Anfrage, diesem Chatroom beizutreten, ablehnen möchten ?"
- "Zugriff verweigern"
+ "Bist du sicher, dass du die Beitrittsanfrage von %1$s zu diesem Chat ablehnen möchtest?"
+ "Zugriff ablehnen""Ablehnen und sperren"
- "Wir konnten diese Anfrage nicht ablehnen. Möchten Sie es noch einmal versuchen?"
- "Anfrage konnte nicht abgelehnt werden"
+ "Wir konnten diese Beitrittsanfrage nicht ablehnen. Möchtest du es noch mal versuchen?"
+ "Beitrittsanfrage konnte nicht abgelehnt werden""Ablehnung der Beitrittsanfrage"
- "Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen."
+ "Sollte jemand um Beitritt zum Chat bitten, kannst du die Anfrage hier sehen.""Keine ausstehende Beitrittsanfrage""Beitrittsanfragen werden geladen …""Beitrittsanfragen"
- "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
- "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
+ "%1$s +%2$d weiterer möchten diesem Chat beitreten"
+ "%1$s +%2$d weitere möchten diesem Chat beitreten""Alles ansehen""Akzeptieren"
- "%1$s möchte diesem Chatroom beitreten"
+ "%1$s möchte diesem Chat beitreten""Ansicht"
diff --git a/features/knockrequests/impl/src/main/res/values-el/translations.xml b/features/knockrequests/impl/src/main/res/values-el/translations.xml
index 2066b2e2930..5c8bec16f88 100644
--- a/features/knockrequests/impl/src/main/res/values-el/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-el/translations.xml
@@ -32,5 +32,4 @@
"Προβολή όλων""Αποδοχή""%1$s θέλει να συμμετάσχει σε αυτή την αίθουσα"
- "Προβολή"
diff --git a/features/knockrequests/impl/src/main/res/values-es/translations.xml b/features/knockrequests/impl/src/main/res/values-es/translations.xml
index ea9f74544de..8f9ab72b703 100644
--- a/features/knockrequests/impl/src/main/res/values-es/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-es/translations.xml
@@ -32,5 +32,4 @@
"Ver todo""Aceptar""%1$s quiere unirse a esta sala"
- "Ver"
diff --git a/features/knockrequests/impl/src/main/res/values-ko/translations.xml b/features/knockrequests/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..ef97ab4a1e1
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,35 @@
+
+
+ "네, 모두 수락합니다"
+ "모든 가입 요청을 정말로 수락하시겠습니까?"
+ "모든 요청 수락"
+ "모두 수락"
+ "모든 요청을 처리할 수 없습니다. 다시 시도하시겠습니까?"
+ "모든 요청을 수락하지 못했습니다."
+ "모든 가입 요청 수락"
+ "이 요청을 수락할 수 없습니다. 다시 시도하시겠습니까?"
+ "요청을 수락하지 못했습니다"
+ "가입 요청 수락"
+ "네, 거절하고 차단합니다"
+ "%1$s 을 거부하고 차단하시겠습니까? 이 사용자는 이 방에 다시 참여하기 위해 액세스를 요청할 수 없습니다."
+ "접근 거부 및 차단"
+ "접근 거부 및 차단"
+ "네, 거절합니다"
+ "%1$s 의 이 방에 대한 요청을 정말 거부하시겠습니까?"
+ "접근 거부"
+ "거부 및 차단"
+ "이 요청을 거부할 수 없습니다. 다시 시도하시겠습니까?"
+ "요청 거부에 실패했습니다"
+ "가입 요청 거부"
+ "누군가가 방에 참여 요청을 한다면, 여기에서 그 요청을 볼 수 있습니다."
+ "보류 중인 가입 요청이 없습니다."
+ "가입 요청을 로딩 중…"
+ "참여 요청"
+
+ "%1$s +%2$d 명이 이 방에 참여하고 싶어합니다."
+
+ "모두 보기"
+ "수락"
+ "%1$s 이 방에 참여하고 싶습니다."
+ "보기"
+
diff --git a/features/knockrequests/impl/src/main/res/values-nl/translations.xml b/features/knockrequests/impl/src/main/res/values-nl/translations.xml
index 46787d05e0c..a78d6222caa 100644
--- a/features/knockrequests/impl/src/main/res/values-nl/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-nl/translations.xml
@@ -1,4 +1,5 @@
"Accepteren"
+ "Bekijken"
diff --git a/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml
index b602cedaf5e..d6fcebb5ab2 100644
--- a/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml
@@ -1,36 +1,36 @@
"Sim, aceitar todos"
- "Tem certeza de que deseja aceitar todos os pedidos de adesão?"
+ "Tem certeza de que deseja aceitar todos os pedidos de entrada?""Aceitar todos os pedidos""Aceitar todos""Não pudemos aceitar todas as solicitações. Você gostaria de tentar novamente?""Falha ao aceitar todas as solicitações"
- "Aceitando todas as solicitações de adesão"
+ "Aceitando todas as solicitações de entrada""Não pudemos aceitar essa solicitação. Você gostaria de tentar novamente?""Falha ao aceitar a solicitação"
- "Aceitando solicitação de adesão"
+ "Aceitando solicitação de entrada""Sim, recusar e banir""Você tem certeza de que deseja recusar e banir %1$s? Este usuário não poderá solicitar acesso para entrar nesta sala novamente.""Recusar e proibir o acesso""Recusando e proibindo o acesso""Sim, recusar"
- "Você tem certeza de que deseja recusar a solicitação de %1$s para participar desta sala?"
+ "Você tem certeza de que deseja recusar a solicitação de %1$s para entrar nesta sala?""Recusar acesso""Recusar e banir""Não foi possível recusar esta solicitação. Você gostaria de tentar novamente?""Falha ao recusar a solicitação"
- "Recusando a solicitação de adesão"
+ "Recusando a solicitação de entrada""Quando alguém pedir para entrar na sala, você poderá ver o pedido aqui."
- "Nenhum pedido pendente de adesão"
- "Carregando solicitações para participar…"
- "Solicitações para entrar"
+ "Nenhum pedido de entrada pendente"
+ "Carregando solicitações de entrada…"
+ "Pedidos de entrada"
- "%1$s +%2$d outro desejam entrar desta sala"
- "%1$s +%2$d outros desejam entrar desta sala"
+ "%1$s + outro %2$d desejam entrar nesta sala"
+ "%1$s + outros %2$d desejam entrar nesta sala""Ver tudo""Aceitar""%1$s quer entrar nesta sala"
- "Ver"
+ "Visualizar"
diff --git a/features/knockrequests/impl/src/main/res/values-ro/translations.xml b/features/knockrequests/impl/src/main/res/values-ro/translations.xml
index 2a75681c5d0..06bfbc96745 100644
--- a/features/knockrequests/impl/src/main/res/values-ro/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-ro/translations.xml
@@ -1,4 +1,37 @@
+ "Da, acceptati tot"
+ "Sunteți sigur că doriți să acceptați toate cererile de alăturare?"
+ "Acceptați toate cererile"
+ "Acceptați tot"
+ "Nu am putut accepta toate cererile. Doriți să încercați din nou?"
+ "Nu s-au putut accepta toate cererile"
+ "Se acceptă toate cererile de alăturare"
+ "Nu am putut accepta această cerere. Doriți să încercați din nou?"
+ "Nu s-a putut accepta cererea"
+ "Se acceptă cererea de alăturare"
+ "Da, refuzați și interziceți"
+ "Sunteți sigur că doriți să refuzați și să interziceți accesul lui %1$s? Acest utilizator nu va mai putea cere accesul pentru a se alătura acestei camere."
+ "Refuzați și interziceți accesul"
+ "Se refuză și interzice accesul"
+ "Da, refuzați"
+ "Sunteți sigur că doriți să refuzați cererea %1$s de a vă alătura acestei camere?"
+ "Refuzați accesul"
+ "Refuzați și interziceți"
+ "Nu am putut refuza această cerere. Doriți să încercați din nou?"
+ "Cererea nu a putut fi respinsă"
+ "Se refuza cererea de alăturare"
+ "Când cineva va cere să se alăture camerei, veți putea vedea cererea aici."
+ "Nu există cereri de alăturare în așteptare"
+ "Se încarcă cererile de alăturare…"
+ "Cereri de alăturare"
+
+ "%1$s +%2$d utilizator doresc să se alăture acestei camere"
+ "%1$s +%2$d utilizatori doresc să se alăture acestei camere"
+ "%1$s +%2$d utilizatori doresc să se alăture acestei camere"
+
+ "Vizualizați tot""Acceptați"
+ "%1$s dorește să se alăture acestei camere"
+ "Vizualizați"
diff --git a/features/knockrequests/impl/src/main/res/values-ru/translations.xml b/features/knockrequests/impl/src/main/res/values-ru/translations.xml
index f5e91a469a5..8080d273fb9 100644
--- a/features/knockrequests/impl/src/main/res/values-ru/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-ru/translations.xml
@@ -11,7 +11,7 @@
"Не удалось принять запрос""Принятие заявки на присоединение""Да, отклонить и запретить"
- "Вы уверен, что хочешь отклонить и запретить %1$s? Этот пользователь больше не сможет запросить доступ к этой комнате."
+ "Вы уверены, что хотите отклонить и запретить %1$s? Этот пользователь больше не сможет запросить доступ к этой комнате.""Отклонить и запретить доступ""Отклонение и запрет доступа""Да, отклонить"
diff --git a/features/knockrequests/impl/src/main/res/values-tr/translations.xml b/features/knockrequests/impl/src/main/res/values-tr/translations.xml
index b6a070ee08b..445d2aa2ddd 100644
--- a/features/knockrequests/impl/src/main/res/values-tr/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-tr/translations.xml
@@ -32,5 +32,4 @@
"Tümünü görüntüle""Kabul et""%1$s bu odaya katılmak istiyor"
- "Görüntüle"
diff --git a/features/knockrequests/impl/src/main/res/values-uk/translations.xml b/features/knockrequests/impl/src/main/res/values-uk/translations.xml
index 55c973fbf4e..11a9b7fad4a 100644
--- a/features/knockrequests/impl/src/main/res/values-uk/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-uk/translations.xml
@@ -33,5 +33,5 @@
"Переглянути все""Прийняти""%1$s хоче приєднатися до цієї кімнати"
- "Перегляд"
+ "Переглянути"
diff --git a/features/knockrequests/impl/src/main/res/values-uz/translations.xml b/features/knockrequests/impl/src/main/res/values-uz/translations.xml
index d7e6705e67c..8cad1166df2 100644
--- a/features/knockrequests/impl/src/main/res/values-uz/translations.xml
+++ b/features/knockrequests/impl/src/main/res/values-uz/translations.xml
@@ -1,4 +1,5 @@
"Qabul qiling"
+ "Ko\'rish"
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
index 252a0ceaeb7..9eaa51cecb8 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
index e5dbb394476..a9fea0905ee 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.kt
new file mode 100644
index 00000000000..2ef357c7c72
--- /dev/null
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPointTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.tests.testutils.node.TestParentNode
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultKnockRequestsListEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node builder`() = runTest {
+ val entryPoint = DefaultKnockRequestsListEntryPoint()
+
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ KnockRequestsListNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenter = createKnockRequestsListPresenter(),
+ )
+ }
+ val result = entryPoint.createNode(parentNode, BuildContext.root(null))
+ assertThat(result).isInstanceOf(KnockRequestsListNode::class.java)
+ }
+}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
index 2d33642a06c..b0d0b68c917 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -286,19 +287,19 @@ class KnockRequestsListPresenterTest {
assert(acceptFailureLambda).isCalledOnce()
assert(acceptSuccessLambda).isCalledOnce()
}
+}
- private fun TestScope.createKnockRequestsListPresenter(
- canAccept: Boolean = true,
- canDecline: Boolean = true,
- canBan: Boolean = true,
- knockRequestsFlow: Flow> = flowOf(emptyList())
- ): KnockRequestsListPresenter {
- val knockRequestsService = KnockRequestsService(
- knockRequestsFlow = knockRequestsFlow,
- coroutineScope = backgroundScope,
- isKnockFeatureEnabledFlow = flowOf(true),
- permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
- )
- return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
- }
+internal fun TestScope.createKnockRequestsListPresenter(
+ canAccept: Boolean = true,
+ canDecline: Boolean = true,
+ canBan: Boolean = true,
+ knockRequestsFlow: Flow> = flowOf(emptyList())
+): KnockRequestsListPresenter {
+ val knockRequestsService = KnockRequestsService(
+ knockRequestsFlow = knockRequestsFlow,
+ coroutineScope = backgroundScope,
+ isKnockFeatureEnabledFlow = flowOf(true),
+ permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
+ )
+ return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
index 5b4b8fb7897..188dcc7e563 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/knockrequests/test/build.gradle.kts b/features/knockrequests/test/build.gradle.kts
new file mode 100644
index 00000000000..dc3407da014
--- /dev/null
+++ b/features/knockrequests/test/build.gradle.kts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.knockrequests.test"
+}
+
+dependencies {
+ implementation(projects.features.knockrequests.api)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/knockrequests/test/src/main/kotlin/io/element/android/features/knockrequests/test/FakeKnockRequestsListEntryPoint.kt b/features/knockrequests/test/src/main/kotlin/io/element/android/features/knockrequests/test/FakeKnockRequestsListEntryPoint.kt
new file mode 100644
index 00000000000..95fc36d693f
--- /dev/null
+++ b/features/knockrequests/test/src/main/kotlin/io/element/android/features/knockrequests/test/FakeKnockRequestsListEntryPoint.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeKnockRequestsListEntryPoint : KnockRequestsListEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ ): Node = lambdaError()
+}
diff --git a/features/leaveroom/api/build.gradle.kts b/features/leaveroom/api/build.gradle.kts
index e988d87c237..b548081653b 100644
--- a/features/leaveroom/api/build.gradle.kts
+++ b/features/leaveroom/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
@@ -14,7 +15,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
- implementation(projects.libraries.designsystem)
- implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrix.api)
}
diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt
index 11f1c5d20cd..6d228ee8875 100644
--- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt
+++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,9 +10,6 @@ package io.element.android.features.leaveroom.api
import io.element.android.libraries.matrix.api.core.RoomId
-sealed interface LeaveRoomEvent {
- data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
- data object HideConfirmation : LeaveRoomEvent
- data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
- data object HideError : LeaveRoomEvent
+interface LeaveRoomEvent {
+ data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
}
diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt
new file mode 100644
index 00000000000..852193d8749
--- /dev/null
+++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.api
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.libraries.matrix.api.core.RoomId
+
+fun interface LeaveRoomRenderer {
+ @Composable
+ fun Render(
+ state: LeaveRoomState,
+ onSelectNewOwners: (RoomId) -> Unit,
+ modifier: Modifier,
+ )
+}
diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt
index cf8f8da9b61..92a59168587 100644
--- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt
+++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt
@@ -1,35 +1,16 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.leaveroom.api
-import io.element.android.libraries.matrix.api.core.RoomId
+import androidx.compose.runtime.Immutable
-data class LeaveRoomState(
- val confirmation: Confirmation,
- val progress: Progress,
- val error: Error,
- val eventSink: (LeaveRoomEvent) -> Unit,
-) {
- sealed interface Confirmation {
- data object Hidden : Confirmation
- data class Dm(val roomId: RoomId) : Confirmation
- data class Generic(val roomId: RoomId) : Confirmation
- data class PrivateRoom(val roomId: RoomId) : Confirmation
- data class LastUserInRoom(val roomId: RoomId) : Confirmation
- }
-
- sealed interface Progress {
- data object Hidden : Progress
- data object Shown : Progress
- }
-
- sealed interface Error {
- data object Hidden : Error
- data object Shown : Error
- }
+@Immutable
+interface LeaveRoomState {
+ val eventSink: (LeaveRoomEvent) -> Unit
}
diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt
deleted file mode 100644
index e5bcbfcf45c..00000000000
--- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.leaveroom.api
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.libraries.matrix.api.core.RoomId
-
-class LeaveRoomStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.Hidden,
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Hidden,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.Generic(roomId = A_ROOM_ID),
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Hidden,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.PrivateRoom(roomId = A_ROOM_ID),
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Hidden,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Hidden,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.Hidden,
- progress = LeaveRoomState.Progress.Shown,
- error = LeaveRoomState.Error.Hidden,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.Hidden,
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Shown,
- ),
- aLeaveRoomState(
- confirmation = LeaveRoomState.Confirmation.Dm(roomId = A_ROOM_ID),
- progress = LeaveRoomState.Progress.Hidden,
- error = LeaveRoomState.Error.Hidden,
- ),
- )
-}
-
-private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
-
-fun aLeaveRoomState(
- confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
- progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
- error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
- eventSink: (LeaveRoomEvent) -> Unit = {},
-) = LeaveRoomState(
- confirmation = confirmation,
- progress = progress,
- error = error,
- eventSink = eventSink,
-)
diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt
deleted file mode 100644
index 28321d1b99e..00000000000
--- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.leaveroom.api
-
-import androidx.annotation.StringRes
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.components.ProgressDialog
-import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
-import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
-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.ui.strings.CommonStrings
-
-@Composable
-fun LeaveRoomView(
- state: LeaveRoomState
-) {
- LeaveRoomConfirmationDialog(state)
- LeaveRoomProgressDialog(state)
- LeaveRoomErrorDialog(state)
-}
-
-@Composable
-private fun LeaveRoomConfirmationDialog(
- state: LeaveRoomState,
-) {
- when (state.confirmation) {
- is LeaveRoomState.Confirmation.Hidden -> {}
-
- is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog(
- text = R.string.leave_room_alert_private_subtitle,
- roomId = state.confirmation.roomId,
- isDm = false,
- eventSink = state.eventSink,
- )
-
- is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
- text = R.string.leave_room_alert_private_subtitle,
- roomId = state.confirmation.roomId,
- isDm = false,
- eventSink = state.eventSink,
- )
-
- is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
- text = R.string.leave_room_alert_empty_subtitle,
- roomId = state.confirmation.roomId,
- isDm = false,
- eventSink = state.eventSink,
- )
-
- is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
- text = R.string.leave_room_alert_subtitle,
- roomId = state.confirmation.roomId,
- isDm = false,
- eventSink = state.eventSink,
- )
- }
-}
-
-@Composable
-private fun LeaveRoomConfirmationDialog(
- @StringRes text: Int,
- roomId: RoomId,
- isDm: Boolean,
- eventSink: (LeaveRoomEvent) -> Unit,
-) {
- ConfirmationDialog(
- title = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
- content = stringResource(text),
- submitText = stringResource(CommonStrings.action_leave),
- onSubmitClick = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
- onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
- )
-}
-
-@Composable
-private fun LeaveRoomProgressDialog(
- state: LeaveRoomState,
-) {
- when (state.progress) {
- is LeaveRoomState.Progress.Hidden -> {}
- is LeaveRoomState.Progress.Shown -> ProgressDialog(
- text = stringResource(CommonStrings.common_leaving_room),
- )
- }
-}
-
-@Composable
-private fun LeaveRoomErrorDialog(
- state: LeaveRoomState,
-) {
- when (state.error) {
- is LeaveRoomState.Error.Hidden -> {}
- is LeaveRoomState.Error.Shown -> ErrorDialog(
- content = stringResource(CommonStrings.error_unknown),
- onSubmit = { state.eventSink(LeaveRoomEvent.HideError) }
- )
- }
-}
-
-@PreviewsDayNight
-@Composable
-internal fun LeaveRoomViewPreview(
- @PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState
-) = ElementPreview {
- Box(
- modifier = Modifier.size(300.dp, 300.dp),
- propagateMinConstraints = true,
- ) {
- LeaveRoomView(state = state)
- }
-}
diff --git a/features/leaveroom/api/src/main/res/values-bg/translations.xml b/features/leaveroom/api/src/main/res/values-bg/translations.xml
index 69d203216a5..8bb88631d53 100644
--- a/features/leaveroom/api/src/main/res/values-bg/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-bg/translations.xml
@@ -1,4 +1,6 @@
+ "Сигурни ли сте, че искате да напуснете тази стая? Вие сте единственият човек тук. Ако напуснете, никой няма да може да се присъедини в бъдеще, включително и вие."
+ "Сигурни ли сте, че искате да напуснете тази стая? Тази стая не е общодостъпна и няма да можете да се присъедините отново без покана.""Сигурни ли сте, че искате да напуснете стаята?"
diff --git a/features/leaveroom/api/src/main/res/values-cs/translations.xml b/features/leaveroom/api/src/main/res/values-cs/translations.xml
index d3163cfbed5..3853553cf11 100644
--- a/features/leaveroom/api/src/main/res/values-cs/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-cs/translations.xml
@@ -3,5 +3,7 @@
"Opravdu chcete opustit tuto konverzaci? Tato konverzace není veřejná a bez pozvánky se k ní nebudete moci znovu připojit.""Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.""Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."
+ "Vyberte vlastníky"
+ "Jste jediným vlastníkem této místnost. Než místnost opustíte, musíte vlastnictví převést na někoho jiného.""Opravdu chcete opustit místnost?"
diff --git a/features/leaveroom/api/src/main/res/values-cy/translations.xml b/features/leaveroom/api/src/main/res/values-cy/translations.xml
index 1ce4f2f3b48..ea02d4d8696 100644
--- a/features/leaveroom/api/src/main/res/values-cy/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-cy/translations.xml
@@ -3,5 +3,8 @@
"Ydych chi\'n siŵr eich bod am adael y sgwrs hon? Dyw\'r sgwrs hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad.""Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Chi yw\'r unig berson yma. Os byddwch yn gadael, fydd neb yn gallu ymuno yn y dyfodol, gan gynnwys chi.""Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Dyw\'r ystafell hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."
+ "Dewiswch Berchnogion"
+ "Chi yw unig berchennog yr ystafell hon. Mae angen i chi drosglwyddo perchnogaeth i rywun arall cyn i chi adael yr room."
+ "Trosglwyddo perchnogaeth""Ydych chi\'n siŵr eich bod am adael yr ystafell?"
diff --git a/features/leaveroom/api/src/main/res/values-da/translations.xml b/features/leaveroom/api/src/main/res/values-da/translations.xml
index 5a1364be0c3..af49087cf79 100644
--- a/features/leaveroom/api/src/main/res/values-da/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-da/translations.xml
@@ -3,5 +3,8 @@
"Er du sikker på, at du vil forlade denne samtale? Denne samtale er ikke offentlig, og du kan ikke deltage igen uden en invitation.""Er du sikker på, at du vil forlade dette rum? Du er den eneste person her. Hvis du går, vil ingen kunne tilslutte sig det i fremtiden, heller ikke dig.""Er du sikker på, at du vil forlade dette rum? Rummet er ikke offentligt, så du vil ikke kunne deltage igen uden en invitation."
+ "Vælg ejere"
+ "Du er den eneste ejer af dette rum. Du skal overføre ejerskabet til en anden, før du forlader rummet."
+ "Overdrag ejerskab""Er du sikker på, at du ønsker at forlade rummet?"
diff --git a/features/leaveroom/api/src/main/res/values-de/translations.xml b/features/leaveroom/api/src/main/res/values-de/translations.xml
index 9a9a53a769d..2a0326d5eae 100644
--- a/features/leaveroom/api/src/main/res/values-de/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-de/translations.xml
@@ -1,7 +1,10 @@
- "Sind Sie sicher, dass Sie diesen Chat verlassen wollen? Dieser Chat ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."
- "Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Sie sind die einzige Person hier. Wenn Sie gehen, kann in Zukunft niemand mehr - auch Sie nicht - diesen Chatrooom betreten.."
- "Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Dieser Chatroom ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."
- "Sind Sie sicher, dass Sie den Raum verlassen möchten?"
+ "Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."
+ "Bist du sicher, dass du diesen Chat verlassen möchtest? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr eintreten, auch du nicht."
+ "Bist du sicher, dass du diesen Chat verlassen möchtest? Dieser Chat ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."
+ "Wähle Eigentümer"
+ "Du bist der einzige Eigentümer dieses Chats. Du musst die Eigentumsrechte an jemand anderen übertragen, bevor du den Chat verlässt."
+ "Eigentumsrechte übertragen"
+ "Bist du sicher, dass du den Chat verlassen willst?"
diff --git a/features/leaveroom/api/src/main/res/values-et/translations.xml b/features/leaveroom/api/src/main/res/values-et/translations.xml
index e0636a37f8e..78fff92fe62 100644
--- a/features/leaveroom/api/src/main/res/values-et/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-et/translations.xml
@@ -3,5 +3,8 @@
"Kas sa oled kindel, et soovid sellest vestlusest lahkuda? See vestlus pole avalik ja uuesti liitumiseks vajad kutset.""Kas sa oled kindel, et soovid sellest jututoast lahkuda? Sa oled siin viimane osaleja ja peale sinu lahkumist ei saa keegi enam liituda, isegi sina mitte.""Kas sa oled kindel, et soovid sellest jututoast lahkuda? See jututuba pole avalik ja uuesti liitumiseks vajad kutset."
+ "Vali omanikud"
+ "Sa oled selle jututoa ainus omanik. Enne jututoast lahkumist pead omandi üle andma kellelegi teisele."
+ "Anna omand üle""Kas sa oled kindel, et soovid sellest jututoast lahkuda?"
diff --git a/features/leaveroom/api/src/main/res/values-eu/translations.xml b/features/leaveroom/api/src/main/res/values-eu/translations.xml
index df735861b84..16fee54c827 100644
--- a/features/leaveroom/api/src/main/res/values-eu/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-eu/translations.xml
@@ -3,5 +3,7 @@
"Ziur elkarrizketa utzi nahi duzula? Ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe.""Ziur gelatik irten nahi duzula? Dagoen pertsona bakarra zara. Ateratzen bazara ezingo da inor batu etorkizunean, ezta zeu ere.""Ziur gelatik irten nahi duzula? Gela hau ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe."
+ "Aukeratu jabeak"
+ "Eskualdatu jabetza""Ziur gelatik atera nahi duzula?"
diff --git a/features/leaveroom/api/src/main/res/values-fa/translations.xml b/features/leaveroom/api/src/main/res/values-fa/translations.xml
index 3c5a4cddecd..167070891f9 100644
--- a/features/leaveroom/api/src/main/res/values-fa/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-fa/translations.xml
@@ -1,6 +1,8 @@
- "آیا مطمئن هستید که می خواهید این اتاق را ترک کنید؟ شما تنها کسی هستید که اینجا هستید. اگر ترک کنید، هیچ کس نمی تواند در آینده به آن بپیوندد.از جمله خود شما."
- "آیا مطمئن هستید که می خواهید از این اتاق خارج شوید؟ این اتاق عمومی نیست و نمیتوانید بدون دعوت دوباره بپیوندید."
- "آیا مطمئن هستید که می خواهید اتاق را ترک کنید؟"
+ "مطمئنید که میخواهید این اتاق را ترک کنید؟ تنها فرد اینجا هستید. در صورت ترک، هیچکسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد."
+ "مطمئنید که میخواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید."
+ "گزینش مالکان"
+ "انتقال مالکیت"
+ "مطمئنید که میخواهید این اتاق را ترک کنید؟"
diff --git a/features/leaveroom/api/src/main/res/values-fi/translations.xml b/features/leaveroom/api/src/main/res/values-fi/translations.xml
index da1ba00ae5a..a97696ceb2e 100644
--- a/features/leaveroom/api/src/main/res/values-fi/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-fi/translations.xml
@@ -3,5 +3,8 @@
"Haluatko varmasti poistua keskustelusta? Tämä keskustelu ei ole julkinen ja et voi liittyä takaisin ilman kutsua.""Haluatko varmasti poistua huoneesta? Olet huoneen ainoa jäsen. Jos poistut, kukaan ei voi liittyä takaisin, et edes sinä.""Haluatko varmasti poistua huoneesta? Tämä huone ei ole julkinen ja et voi liittyä takaisin ilman kutsua."
+ "Valitse omistajat"
+ "Olet tämän huoneen ainoa omistaja. Sinun on siirrettävä omistajuus jollekulle toiselle ennen kuin poistut huoneesta."
+ "Siirrä omistajuus""Haluatko varmasti poistua huoneesta?"
diff --git a/features/leaveroom/api/src/main/res/values-fr/translations.xml b/features/leaveroom/api/src/main/res/values-fr/translations.xml
index 2c801ed76a6..5f75b7ae55f 100644
--- a/features/leaveroom/api/src/main/res/values-fr/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-fr/translations.xml
@@ -3,5 +3,8 @@
"Êtes-vous sûr de vouloir quitter cette discussion ? Vous ne pourrez pas la rejoindre à nouveau sans y être invité.""Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous.""Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."
+ "Choisissez les propriétaires"
+ "Vous êtes le seul propriétaire de ce salon. Vous devez en transférer la propriété à quelqu’un d’autre avant de le quitter."
+ "Transférer la propriété""Êtes-vous sûr de vouloir quitter le salon ?"
diff --git a/features/leaveroom/api/src/main/res/values-hu/translations.xml b/features/leaveroom/api/src/main/res/values-hu/translations.xml
index 8b5136f4955..90eae5cb229 100644
--- a/features/leaveroom/api/src/main/res/values-hu/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-hu/translations.xml
@@ -3,5 +3,8 @@
"Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni.""Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve.""Biztos, hogy elhagyja ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fog tudni újra belépni."
+ "Tulajdonosok kiválasztása"
+ "Ön az egyetlen tulajdonosa ennek a szobának. Mielőtt elhagyja a szobát, át kell adnia a tulajdonjogot valaki másnak."
+ "Tulajdonjog átruházása""Biztos, hogy elhagyja a szobát?"
diff --git a/features/leaveroom/api/src/main/res/values-it/translations.xml b/features/leaveroom/api/src/main/res/values-it/translations.xml
index 1b403e8f935..3eb276166b1 100644
--- a/features/leaveroom/api/src/main/res/values-it/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-it/translations.xml
@@ -3,5 +3,8 @@
"Vuoi davvero abbandonare questa conversazione? La conversazione non è pubblica e non potrai rientrare senza un invito.""Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso.""Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito."
+ "Scegli i proprietari"
+ "Sei l\'unico proprietario di questa stanza. Devi trasferire la proprietà a qualcun altro prima di lasciare la stanza."
+ "Trasferisci proprietà""Sei sicuro di voler lasciare la stanza?"
diff --git a/features/leaveroom/api/src/main/res/values-ko/translations.xml b/features/leaveroom/api/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..71f5bbe47e2
--- /dev/null
+++ b/features/leaveroom/api/src/main/res/values-ko/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "이 대화를 나가시겠습니까? 이 대화는 공개되지 않았으므로 초대 없이는 다시 참여할 수 없습니다."
+ "정말로 이 방을 떠나시겠어요? 이 방에서 유일하게 남은 사용자입니다. 나간 이후부터는 당신을 포함해서 아무도 다시 참여할 수 없어요."
+ "정말로 이 방을 떠나시겠어요? 이 방은 공개가 아니기 때문에 초대 없이는 다시 참여할 수 없습니다."
+ "소유자 선택"
+ "이 방의 유일한 소유자는 귀하입니다. 방을 떠나기 전에 다른 사람에게 소유권을 양도해야 합니다."
+ "소유권 이전"
+ "정말로 이 방을 떠나시겠어요?"
+
diff --git a/features/leaveroom/api/src/main/res/values-nb/translations.xml b/features/leaveroom/api/src/main/res/values-nb/translations.xml
index 1e1800e0d03..7286ca93cf0 100644
--- a/features/leaveroom/api/src/main/res/values-nb/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-nb/translations.xml
@@ -3,5 +3,8 @@
"Er du sikker på at du vil forlate denne samtalen? Denne samtalen er ikke offentlig, og du vil ikke kunne bli med igjen uten en invitasjon.""Er du sikker på at du vil forlate dette rommet? Du er den eneste personen her. Hvis du forlater, vil ingen kunne bli med i fremtiden, inkludert deg.""Er du sikker på at du vil forlate dette rommet? Dette rommet er ikke offentlig, og du vil ikke kunne bli med igjen uten en invitasjon."
+ "Velg eiere"
+ "Du er den eneste eieren av dette rommet. Du må overføre eierskapet til noen andre før du forlater rommet."
+ "Overfør eierskap""Er du sikker på at du vil forlate rommet?"
diff --git a/features/leaveroom/api/src/main/res/values-pl/translations.xml b/features/leaveroom/api/src/main/res/values-pl/translations.xml
index 0fd8769389b..440b8063111 100644
--- a/features/leaveroom/api/src/main/res/values-pl/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-pl/translations.xml
@@ -3,5 +3,8 @@
"Czy na pewno chcesz opuścić tę konwersację? Konwersacja nie jest publiczna i nie będziesz mógł dołączyć ponownie bez zaproszenia.""Jesteś pewien, że chcesz opuścić ten pokój? Jesteś tu jedyną osobą. Jeśli wyjdziesz, nikt nie będzie mógł dołączyć, w tym Ty.""Czy na pewno chcesz opuścić ten pokój? Ten pokój nie jest publiczny i nie będziesz mógł do niego wrócić bez zaproszenia."
+ "Wybierz właścicieli"
+ "Jesteś jedynym właścicielem tego pokoju. Musisz przenieść własność na kogoś innego, zanim go opuścisz."
+ "Przenieś własność""Jesteś pewien, że chcesz wyjść z tego pokoju?"
diff --git a/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml b/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml
index b55010ad807..178e43bef62 100644
--- a/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml
@@ -1,7 +1,10 @@
"Tem certeza de que deseja sair dessa conversa? Essa conversa não é pública e você não poderá entrar novamente sem um convite."
- "Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá ingressar no futuro, inclusive você."
+ "Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá entrar no futuro, até mesmo você.""Tem certeza de que deseja sair desta sala? Esta sala não é pública e você não poderá entrar novamente sem um convite."
+ "Escolher proprietários"
+ "Você é o único proprietário desta sala. Você precisa transferir a posse para outra pessoa antes de sair da sala."
+ "Transferir posse""Tem certeza de que deseja sair da sala?"
diff --git a/features/leaveroom/api/src/main/res/values-pt/translations.xml b/features/leaveroom/api/src/main/res/values-pt/translations.xml
index 7547378598e..4832cd92015 100644
--- a/features/leaveroom/api/src/main/res/values-pt/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-pt/translations.xml
@@ -3,5 +3,8 @@
"Tens a certeza que queres sair desta conversa? Não é pública, logo não poderás voltar a participar sem um convite.""Tens a certeza que queres sair desta sala? És o único participante. Se saíres, ninguém mais poderá entrar, incluindo tu.""Tens a certeza que queres sair desta sala? Atenta que não é pública e portanto não poderás voltar a entrar sem um novo convite."
+ "Escolher donos"
+ "És o único dono/dona desta sala. Se quiseres sair, terás que a transferir para alguém primeiro."
+ "Transferir posse""Tens a certeza que queres sair da sala?"
diff --git a/features/leaveroom/api/src/main/res/values-ro/translations.xml b/features/leaveroom/api/src/main/res/values-ro/translations.xml
index 5af15904397..8f504148f6f 100644
--- a/features/leaveroom/api/src/main/res/values-ro/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-ro/translations.xml
@@ -3,5 +3,8 @@
"Sunteți sigur că doriți să părăsiți această conversație? Această conversație nu este publică și nu veți putea reveni fără o invitație.""Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra.""Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație."
+ "Alegeți proprietari"
+ "Sunteți singurul proprietar al acestei camere. Trebuie să transferați proprietatea către o altă persoană înainte de a părăsi camera."
+ "Transferați proprietatea""Sunteți sigur că vreți să părăsiți camera?"
diff --git a/features/leaveroom/api/src/main/res/values-ru/translations.xml b/features/leaveroom/api/src/main/res/values-ru/translations.xml
index ebf7ac82cde..907b729610f 100644
--- a/features/leaveroom/api/src/main/res/values-ru/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-ru/translations.xml
@@ -3,5 +3,8 @@
"Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете присоединиться к ней без приглашения.""Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас.""Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."
+ "Назначить владельцев"
+ "Вы единственный владелец этой комнаты. Перед тем, как её покинуть, необходимо передать владение кому-нибудь другому."
+ "Передача владения""Вы уверены, что хотите покинуть комнату?"
diff --git a/features/leaveroom/api/src/main/res/values-sk/translations.xml b/features/leaveroom/api/src/main/res/values-sk/translations.xml
index 74ff01b541a..c9eb9c45ba3 100644
--- a/features/leaveroom/api/src/main/res/values-sk/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-sk/translations.xml
@@ -3,5 +3,8 @@
"Ste si istí, že chcete opustiť konverzáciu?""Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás.""Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."
+ "Vybrať vlastníkov"
+ "Ste jediným vlastníkom tejto miestnosti. Pred opustením miestnosti musíte previesť vlastníctvo na niekoho iného."
+ "Preniesť vlastníctvo""Ste si istí, že chcete opustiť miestnosť?"
diff --git a/features/leaveroom/api/src/main/res/values-sv/translations.xml b/features/leaveroom/api/src/main/res/values-sv/translations.xml
index c80d7163294..2d26a6ec322 100644
--- a/features/leaveroom/api/src/main/res/values-sv/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-sv/translations.xml
@@ -3,5 +3,8 @@
"Ä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.""Ä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.""Ä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."
+ "Välj ägare"
+ "Du är den enda ägaren av det här rummet. Du måste överföra ägarskapet till någon annan innan du lämnar rummet."
+ "Överför ägarskap""Är du säker på att du vill lämna rummet?"
diff --git a/features/leaveroom/api/src/main/res/values-uk/translations.xml b/features/leaveroom/api/src/main/res/values-uk/translations.xml
index c26a0bbaaa6..0c7c92bd2c3 100644
--- a/features/leaveroom/api/src/main/res/values-uk/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-uk/translations.xml
@@ -3,5 +3,8 @@
"Ви впевнені, що хочете залишити цю розмову? Ця розмова не загальнодоступна, і ви не зможете знову приєднатися без запрошення.""Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі й ви.""Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не загальнодоступна, і ви не зможете повернутися до неї без запрошення."
+ "Оберіть власників"
+ "Ви є єдиним власником цієї кімнати. Перед тим як вийти з кімнати, вам потрібно передати право власності іншій особі."
+ "Передати право власності""Ви впевнені, що хочете вийти з кімнати?"
diff --git a/features/leaveroom/api/src/main/res/values-uz/translations.xml b/features/leaveroom/api/src/main/res/values-uz/translations.xml
index 59c111e2acf..374fd347fc6 100644
--- a/features/leaveroom/api/src/main/res/values-uz/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-uz/translations.xml
@@ -1,5 +1,6 @@
+ "Bu suhbatni tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu suhbat hammaga ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz.""Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Siz bu yerda yagona odamsiz. Agar siz tark etsangiz, kelajakda hech kim qo\'shila olmaydi, jumladan siz ham.""Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu xona ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz.""Xonani tark etmoqchi ekanligingizga ishonchingiz komilmi?"
diff --git a/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml b/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
index f561b08adcf..96e3b576c4a 100644
--- a/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-zh-rTW/translations.xml
@@ -3,5 +3,8 @@
"您確定要離開對話嗎?此對話不是公開的,如果沒有收到邀請,您無法重新加入。""您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。""您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"
+ "選擇擁有者"
+ "您是此聊天室的唯一擁有者。離開聊天室前,您必須將所有權移轉給其他人。"
+ "轉移所有權""您確定要離開聊天室嗎?"
diff --git a/features/leaveroom/api/src/main/res/values-zh/translations.xml b/features/leaveroom/api/src/main/res/values-zh/translations.xml
index e9dcf44fd76..6b7f17558bf 100644
--- a/features/leaveroom/api/src/main/res/values-zh/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-zh/translations.xml
@@ -3,5 +3,8 @@
"您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。""确定要离开此聊天室吗?此处只有你一个人。如果离开此聊天室,包括你在内的所有人都将无法进入。""确定要离开此聊天室吗?此聊天室不公开,没有邀请你将无法重新加入。"
+ "选择所有者"
+ "您是本房间的唯一所有者。离开房间前,您需要将所有权转移给他人。"
+ "转让所有权""确定要离开聊天室吗?"
diff --git a/features/leaveroom/api/src/main/res/values/localazy.xml b/features/leaveroom/api/src/main/res/values/localazy.xml
index f068e772bb7..5f9ab3a6195 100644
--- a/features/leaveroom/api/src/main/res/values/localazy.xml
+++ b/features/leaveroom/api/src/main/res/values/localazy.xml
@@ -3,5 +3,8 @@
"Are you sure that you want to leave this conversation? This conversation is not public and you won\'t be able to rejoin without an invite.""Are you sure that you want to leave this room? You\'re the only person here. If you leave, no one will be able to join in the future, including you.""Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite."
+ "Choose owners"
+ "You\'re the only owner of this room. You need to transfer ownership to someone else before you leave the room."
+ "Transfer ownership""Are you sure that you want to leave the room?"
diff --git a/features/leaveroom/impl/build.gradle.kts b/features/leaveroom/impl/build.gradle.kts
index d346897c75e..cc07e668c1a 100644
--- a/features/leaveroom/impl/build.gradle.kts
+++ b/features/leaveroom/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,20 +17,19 @@ android {
namespace = "io.element.android.features.leaveroom.impl"
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
+ api(projects.features.leaveroom.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
- api(projects.features.leaveroom.api)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.push.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
+ testCommonDependencies(libs)
testImplementation(libs.coroutines.core)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
+ testImplementation(projects.libraries.push.test)
}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomEvent.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomEvent.kt
new file mode 100644
index 00000000000..37ec70e0650
--- /dev/null
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomEvent.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.impl
+
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
+
+sealed interface InternalLeaveRoomEvent : LeaveRoomEvent {
+ data object ResetState : InternalLeaveRoomEvent
+}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt
new file mode 100644
index 00000000000..c9335bbf084
--- /dev/null
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomRenderer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.leaveroom.api.LeaveRoomRenderer
+import io.element.android.features.leaveroom.api.LeaveRoomState
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.RoomId
+
+@ContributesBinding(SessionScope::class)
+class InternalLeaveRoomRenderer : LeaveRoomRenderer {
+ @Composable
+ override fun Render(state: LeaveRoomState, onSelectNewOwners: (RoomId) -> Unit, modifier: Modifier) {
+ if (state is InternalLeaveRoomState) {
+ LeaveRoomView(state, onSelectNewOwners)
+ } else {
+ error("Unsupported state type ${state.javaClass}")
+ }
+ }
+}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomState.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomState.kt
new file mode 100644
index 00000000000..68607f21d7c
--- /dev/null
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.impl
+
+import androidx.compose.runtime.Immutable
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
+import io.element.android.features.leaveroom.api.LeaveRoomState
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.core.RoomId
+
+data class InternalLeaveRoomState(
+ val leaveAction: AsyncAction,
+ override val eventSink: (LeaveRoomEvent) -> Unit
+) : LeaveRoomState
+
+@Immutable
+sealed interface Confirmation : AsyncAction.Confirming {
+ data class Dm(val roomId: RoomId) : Confirmation
+ data class Generic(val roomId: RoomId) : Confirmation
+ data class PrivateRoom(val roomId: RoomId) : Confirmation
+ data class LastUserInRoom(val roomId: RoomId) : Confirmation
+ data class LastOwnerInRoom(val roomId: RoomId) : Confirmation
+}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt
new file mode 100644
index 00000000000..769015f3013
--- /dev/null
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.impl
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.matrix.api.core.RoomId
+
+class InternalLeaveRoomStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aLeaveRoomState(),
+ aLeaveRoomState(
+ leaveAction = Confirmation.Generic(roomId = A_ROOM_ID),
+ ),
+ aLeaveRoomState(
+ leaveAction = Confirmation.PrivateRoom(roomId = A_ROOM_ID),
+ ),
+ aLeaveRoomState(
+ leaveAction = Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
+ ),
+ aLeaveRoomState(
+ leaveAction = Confirmation.Dm(roomId = A_ROOM_ID),
+ ),
+ aLeaveRoomState(
+ leaveAction = Confirmation.LastOwnerInRoom(roomId = A_ROOM_ID),
+ ),
+ aLeaveRoomState(
+ leaveAction = AsyncAction.Loading,
+ ),
+ aLeaveRoomState(
+ leaveAction = AsyncAction.Failure(RuntimeException("Something went wrong")),
+ ),
+ )
+}
+
+private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
+
+fun aLeaveRoomState(
+ leaveAction: AsyncAction = AsyncAction.Uninitialized,
+ eventSink: (LeaveRoomEvent) -> Unit = {},
+) = InternalLeaveRoomState(
+ leaveAction = leaveAction,
+ eventSink = eventSink,
+)
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
index 915d65b71fc..c371d425c15 100644
--- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,94 +13,94 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import dev.zacsweers.metro.Inject
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
-import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm
-import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
-import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
-import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.BaseRoom
+import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
+import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
+import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
-import javax.inject.Inject
-class LeaveRoomPresenter @Inject constructor(
+@Inject
+class LeaveRoomPresenter(
private val client: MatrixClient,
private val dispatchers: CoroutineDispatchers,
+ private val notificationConversationService: NotificationConversationService,
) : Presenter {
@Composable
override fun present(): LeaveRoomState {
val scope = rememberCoroutineScope()
- val confirmation = remember { mutableStateOf(LeaveRoomState.Confirmation.Hidden) }
- val progress = remember { mutableStateOf(LeaveRoomState.Progress.Hidden) }
- val error = remember { mutableStateOf(LeaveRoomState.Error.Hidden) }
-
- return LeaveRoomState(
- confirmation = confirmation.value,
- progress = progress.value,
- error = error.value,
+ val leaveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
+ return InternalLeaveRoomState(
+ leaveAction = leaveAction.value,
) { event ->
when (event) {
- is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) {
- showLeaveRoomAlert(
- matrixClient = client,
- roomId = event.roomId,
- confirmation = confirmation,
- )
- }
-
- is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden
- is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) {
- client.leaveRoom(
- roomId = event.roomId,
- confirmation = confirmation,
- progress = progress,
- error = error,
- )
- }
-
- is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden
+ is LeaveRoomEvent.LeaveRoom ->
+ if (event.needsConfirmation) {
+ scope.showLeaveRoomAlert(roomId = event.roomId, leaveAction = leaveAction)
+ } else {
+ scope.leaveRoom(roomId = event.roomId, leaveAction = leaveAction)
+ }
+ InternalLeaveRoomEvent.ResetState -> leaveAction.value = AsyncAction.Uninitialized
}
}
}
-}
-private suspend fun showLeaveRoomAlert(
- matrixClient: MatrixClient,
- roomId: RoomId,
- confirmation: MutableState,
-) {
- matrixClient.getRoom(roomId)?.use { room ->
- val roomInfo = room.roomInfoFlow.first()
- confirmation.value = when {
- roomInfo.isDm -> Dm(roomId)
- // If unknown, assume the room is private
- roomInfo.isPublic == null || roomInfo.isPublic == false -> PrivateRoom(roomId)
- roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId)
- else -> Generic(roomId)
+ private fun CoroutineScope.showLeaveRoomAlert(
+ roomId: RoomId,
+ leaveAction: MutableState>,
+ ) = launch(dispatchers.io) {
+ client.getRoom(roomId)?.use { room ->
+ val roomInfo = room.roomInfoFlow.first()
+ leaveAction.value = when {
+ roomInfo.isDm -> Confirmation.Dm(roomId)
+ room.isLastOwner() && roomInfo.joinedMembersCount > 1L -> Confirmation.LastOwnerInRoom(roomId)
+ // If unknown, assume the room is private
+ roomInfo.isPublic == null || roomInfo.isPublic == false -> Confirmation.PrivateRoom(roomId)
+ roomInfo.joinedMembersCount == 1L -> Confirmation.LastUserInRoom(roomId)
+ else -> Confirmation.Generic(roomId)
+ }
}
}
-}
-private suspend fun MatrixClient.leaveRoom(
- roomId: RoomId,
- confirmation: MutableState,
- progress: MutableState,
- error: MutableState,
-) {
- confirmation.value = LeaveRoomState.Confirmation.Hidden
- progress.value = LeaveRoomState.Progress.Shown
- getRoom(roomId)?.use { room ->
- room.leave()
- .onFailure {
- Timber.e(it, "Error while leaving room ${room.roomId}")
- error.value = LeaveRoomState.Error.Shown
+ private fun CoroutineScope.leaveRoom(
+ roomId: RoomId,
+ leaveAction: MutableState>,
+ ) = launch(dispatchers.io) {
+ leaveAction.runCatchingUpdatingState {
+ client.getRoom(roomId)!!.use { room ->
+ room
+ .leave()
+ .onSuccess { notificationConversationService.onLeftRoom(client.sessionId, roomId) }
+ .onFailure { Timber.e(it, "Error while leaving room ${room.roomId}") }
+ .getOrThrow()
}
+ }
+ }
+
+ private suspend fun BaseRoom.isLastOwner(): Boolean {
+ if (roomInfoFlow.value.isDm) {
+ // DMs are not owned by the user, so we can return false
+ return false
+ } else {
+ val hasPrivilegedCreatorRole = roomInfoFlow.value.privilegedCreatorRole
+ if (!hasPrivilegedCreatorRole) return false
+
+ val creators = usersWithRole(RoomMember.Role.Owner(isCreator = true)).first()
+ val superAdmins = usersWithRole(RoomMember.Role.Owner(isCreator = false)).first()
+ val owners = creators + superAdmins
+ return owners.size == 1 && owners.first().userId == sessionId
+ }
}
- progress.value = LeaveRoomState.Progress.Hidden
}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt
new file mode 100644
index 00000000000..131801b0142
--- /dev/null
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.leaveroom.impl
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.leaveroom.api.LeaveRoomEvent
+import io.element.android.features.leaveroom.api.R
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
+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.ui.strings.CommonStrings
+
+@Suppress("LambdaParameterEventTrailing")
+@Composable
+fun LeaveRoomView(
+ state: InternalLeaveRoomState,
+ onSelectNewOwners: (RoomId) -> Unit,
+) {
+ AsyncActionView(
+ state.leaveAction,
+ onSuccess = {
+ state.eventSink(InternalLeaveRoomEvent.ResetState)
+ },
+ onErrorDismiss = {
+ state.eventSink(InternalLeaveRoomEvent.ResetState)
+ },
+ confirmationDialog = { confirmation ->
+ if (confirmation is Confirmation) {
+ LeaveRoomConfirmationDialog(
+ confirmation = confirmation,
+ eventSink = state.eventSink,
+ onSelectNewOwners = onSelectNewOwners,
+ )
+ }
+ },
+ errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
+ errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
+ progressDialog = { LeaveRoomProgressDialog() },
+ )
+}
+
+@Composable
+private fun LeaveRoomConfirmationDialog(
+ confirmation: Confirmation,
+ eventSink: (LeaveRoomEvent) -> Unit,
+ onSelectNewOwners: (RoomId) -> Unit,
+) {
+ val defaultOnSubmitClick = { roomId: RoomId -> { eventSink(LeaveRoomEvent.LeaveRoom(roomId, needsConfirmation = false)) } }
+ val defaultDismissAction = { eventSink(InternalLeaveRoomEvent.ResetState) }
+ when (confirmation) {
+ is Confirmation.Dm -> LeaveRoomConfirmationDialog(
+ text = stringResource(R.string.leave_room_alert_private_subtitle),
+ isDm = false,
+ onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
+ onDismiss = defaultDismissAction,
+ )
+
+ is Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
+ text = stringResource(R.string.leave_room_alert_private_subtitle),
+ isDm = false,
+ onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
+ onDismiss = defaultDismissAction,
+ )
+
+ is Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
+ text = stringResource(R.string.leave_room_alert_empty_subtitle),
+ isDm = false,
+ onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
+ onDismiss = defaultDismissAction,
+ )
+
+ is Confirmation.LastOwnerInRoom -> LeaveRoomConfirmationDialog(
+ title = stringResource(R.string.leave_room_alert_select_new_owner_title),
+ text = stringResource(R.string.leave_room_alert_select_new_owner_subtitle),
+ isDm = false,
+ submitText = stringResource(R.string.leave_room_alert_select_new_owner_action),
+ destructiveSubmit = true,
+ onSubmitClick = {
+ onSelectNewOwners(confirmation.roomId)
+ eventSink(InternalLeaveRoomEvent.ResetState)
+ },
+ onDismiss = defaultDismissAction,
+ )
+
+ is Confirmation.Generic -> LeaveRoomConfirmationDialog(
+ text = stringResource(R.string.leave_room_alert_subtitle),
+ isDm = false,
+ onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
+ onDismiss = defaultDismissAction,
+ )
+ }
+}
+
+@Composable
+private fun LeaveRoomConfirmationDialog(
+ isDm: Boolean,
+ text: String,
+ onSubmitClick: () -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
+ submitText: String = stringResource(CommonStrings.action_leave),
+ destructiveSubmit: Boolean = false,
+) {
+ ConfirmationDialog(
+ title = title,
+ content = text,
+ submitText = submitText,
+ onSubmitClick = onSubmitClick,
+ onDismiss = onDismiss,
+ destructiveSubmit = destructiveSubmit,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun LeaveRoomProgressDialog(modifier: Modifier = Modifier) {
+ ProgressDialog(
+ text = stringResource(CommonStrings.common_leaving_room),
+ modifier = modifier,
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun LeaveRoomViewPreview(
+ @PreviewParameter(InternalLeaveRoomStateProvider::class) state: InternalLeaveRoomState
+) = ElementPreview {
+ Box(
+ modifier = Modifier.size(300.dp, 300.dp),
+ propagateMinConstraints = true,
+ ) {
+ LeaveRoomView(state = state, onSelectNewOwners = {})
+ }
+}
diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt
index 840c7a9eec4..db5b1416ee6 100644
--- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt
+++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt
@@ -1,22 +1,23 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.leaveroom.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
-@Module
+@BindingContainer
interface LeaveRoomModule {
@Binds
fun bindLeaveRoomPresenter(presenter: LeaveRoomPresenter): Presenter
diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
index 108c2ddb53b..59d2c1ce231 100644
--- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
+++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,17 +13,20 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.leaveroom.api.LeaveRoomEvent
-import io.element.android.features.leaveroom.api.LeaveRoomState
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
+import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@@ -36,15 +40,12 @@ class LeaveBaseRoomPresenterTest {
@Test
fun `present - initial state hides all dialogs`() = runTest {
- val presenter = createLeaveRoomPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden)
- assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
- assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden)
- }
+ createLeaveRoomPresenter()
+ .stateFlow()
+ .test {
+ val initialState = awaitItem()
+ assertThat(initialState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
+ }
}
@Test
@@ -59,13 +60,11 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
val confirmationState = awaitItem()
- assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID))
+ assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Generic(A_ROOM_ID))
}
}
@@ -81,13 +80,11 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
val confirmationState = awaitItem()
- assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID))
+ assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.PrivateRoom(A_ROOM_ID))
}
}
@@ -103,13 +100,11 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
val confirmationState = awaitItem()
- assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID))
+ assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.LastUserInRoom(A_ROOM_ID))
}
}
@@ -125,13 +120,11 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
val confirmationState = awaitItem()
- assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID))
+ assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Dm(A_ROOM_ID))
}
}
@@ -148,11 +141,9 @@ class LeaveBaseRoomPresenterTest {
)
},
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents()
assert(leaveRoomLambda)
@@ -173,44 +164,19 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
- skipItems(1) // Skip show progress state
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
+ val progressState = awaitItem()
+ assertThat(progressState.leaveAction).isEqualTo(AsyncAction.Loading)
val errorState = awaitItem()
- assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
+ assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
cancelAndIgnoreRemainingEvents()
}
}
@Test
- fun `present - show progress indicator while leaving a room`() = runTest {
- val presenter = createLeaveRoomPresenter(
- client = FakeMatrixClient().apply {
- givenGetRoomResult(
- roomId = A_ROOM_ID,
- result = FakeBaseRoom(
- leaveRoomLambda = { Result.success(Unit) }
- ),
- )
- }
- )
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
- val progressState = awaitItem()
- assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown)
- val finalState = awaitItem()
- assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
- }
- }
-
- @Test
- fun `present - hide error hides the error`() = runTest {
+ fun `present - reset state after error`() = runTest {
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
@@ -221,20 +187,23 @@ class LeaveBaseRoomPresenterTest {
)
}
)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
+ presenter.stateFlow().test {
val initialState = awaitItem()
- initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
+ initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
skipItems(1) // Skip show progress state
val errorState = awaitItem()
- assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
- skipItems(1) // Skip hide progress state
- errorState.eventSink(LeaveRoomEvent.HideError)
+ assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
+ errorState.eventSink(InternalLeaveRoomEvent.ResetState)
val hiddenErrorState = awaitItem()
- assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden)
+ assertThat(hiddenErrorState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
}
}
+
+ private fun LeaveRoomPresenter.stateFlow(): Flow {
+ return moleculeFlow(RecompositionMode.Immediate) {
+ present()
+ }.filterIsInstance(InternalLeaveRoomState::class)
+ }
}
private fun TestScope.createLeaveRoomPresenter(
@@ -242,4 +211,5 @@ private fun TestScope.createLeaveRoomPresenter(
): LeaveRoomPresenter = LeaveRoomPresenter(
client = client,
dispatchers = testCoroutineDispatchers(false),
+ notificationConversationService = FakeNotificationConversationService(),
)
diff --git a/features/licenses/api/build.gradle.kts b/features/licenses/api/build.gradle.kts
index 0774deb56be..fcbf81f2109 100644
--- a/features/licenses/api/build.gradle.kts
+++ b/features/licenses/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
index e4beebe6ef3..835af35ca69 100644
--- a/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
+++ b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
@@ -1,15 +1,13 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.licenses.api
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
+import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
-interface OpenSourceLicensesEntryPoint {
- fun getNode(node: Node, buildContext: BuildContext): Node
-}
+interface OpenSourceLicensesEntryPoint : SimpleFeatureEntryPoint
diff --git a/features/licenses/impl/build.gradle.kts b/features/licenses/impl/build.gradle.kts
index 52589aeaf58..52f21ac0bd0 100644
--- a/features/licenses/impl/build.gradle.kts
+++ b/features/licenses/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -17,7 +19,7 @@ android {
namespace = "io.element.android.features.licenses.impl"
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
implementation(libs.serialization.json)
@@ -26,12 +28,8 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings)
api(projects.features.licenses.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
+
+ testCommonDependencies(libs)
testImplementation(libs.coroutines.core)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
index 170e819ce74..671510e6bbc 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,15 +10,14 @@ package io.element.android.features.licenses.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
- override fun getNode(node: Node, buildContext: BuildContext): Node {
- return node.createNode(buildContext)
+class DefaultOpenSourcesLicensesEntryPoint : OpenSourceLicensesEntryPoint {
+ override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
+ return parentNode.createNode(buildContext)
}
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
index 91cab27de9f..34e734a64f9 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,20 +16,21 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-class DependenciesFlowNode @AssistedInject constructor(
+@AssistedInject
+class DependenciesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
) : BaseFlowNode(
@@ -51,7 +53,7 @@ class DependenciesFlowNode @AssistedInject constructor(
return when (navTarget) {
is NavTarget.LicensesList -> {
val callback = object : DependencyLicensesListNode.Callback {
- override fun onOpenLicense(license: DependencyLicenseItem) {
+ override fun navigateToLicense(license: DependencyLicenseItem) {
backstack.push(NavTarget.LicenseDetails(license))
}
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
index 4c4db861e9e..7ae6fa740a0 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
@@ -1,30 +1,30 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.licenses.impl
import android.content.Context
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
-import javax.inject.Inject
interface LicensesProvider {
suspend fun provides(): List
}
@ContributesBinding(AppScope::class)
-class AssetLicensesProvider @Inject constructor(
+class AssetLicensesProvider(
@ApplicationContext private val context: Context,
private val dispatchers: CoroutineDispatchers,
) : LicensesProvider {
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
index ae9ad03e9dc..83e7ae231f2 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,16 +13,17 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
-class DependenciesDetailsNode @AssistedInject constructor(
+@AssistedInject
+class DependenciesDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
) : Node(
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
index 063579414fc..f463a6b8d13 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt
index d1c8a88717d..bf53b05d181 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
index 039ac24b206..424dad415e2 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,15 +13,16 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
-import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class)
-class DependencyLicensesListNode @AssistedInject constructor(
+@AssistedInject
+class DependencyLicensesListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: DependencyLicensesListPresenter,
@@ -29,13 +31,10 @@ class DependencyLicensesListNode @AssistedInject constructor(
plugins = plugins
) {
interface Callback : Plugin {
- fun onOpenLicense(license: DependencyLicenseItem)
+ fun navigateToLicense(license: DependencyLicenseItem)
}
- private fun onOpenLicense(license: DependencyLicenseItem) {
- plugins()
- .forEach { it.onOpenLicense(license) }
- }
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
@@ -43,7 +42,7 @@ class DependencyLicensesListNode @AssistedInject constructor(
DependencyLicensesListView(
state = state,
onBackClick = ::navigateUp,
- onOpenLicense = ::onOpenLicense,
+ onOpenLicense = callback::navigateToLicense,
)
}
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
index a0d3a19ed2b..fe6973958e5 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,16 +14,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.licenses.impl.LicensesProvider
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
-import javax.inject.Inject
+import kotlinx.collections.immutable.toImmutableList
-class DependencyLicensesListPresenter @Inject constructor(
+@Inject
+class DependencyLicensesListPresenter(
private val licensesProvider: LicensesProvider,
) : Presenter {
@Composable
@@ -36,7 +38,7 @@ class DependencyLicensesListPresenter @Inject constructor(
var filter by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
runCatchingExceptions {
- licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
+ licenses = AsyncData.Success(licensesProvider.provides().toImmutableList())
}.onFailure {
licenses = AsyncData.Failure(it)
}
@@ -49,16 +51,16 @@ class DependencyLicensesListPresenter @Inject constructor(
it.safeName.contains(safeFilter, ignoreCase = true) ||
it.groupId.contains(safeFilter, ignoreCase = true) ||
it.artifactId.contains(safeFilter, ignoreCase = true)
- }.toPersistentList())
+ }.toImmutableList())
} else {
filteredLicenses = licenses
}
}
- fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
- when (dependencyLicensesListEvent) {
+ fun handleEvent(event: DependencyLicensesListEvent) {
+ when (event) {
is DependencyLicensesListEvent.SetFilter -> {
- filter = dependencyLicensesListEvent.filter
+ filter = event.filter
}
}
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
index 7e9be25b5aa..d3aeedeb578 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
index 2bd6a5a88ee..6a74cc01ebc 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
index 3419665b659..f60c28ef2ba 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
index 6599768acc3..d26d062d573 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.kt
new file mode 100644
index 00000000000..4b6e3d98efc
--- /dev/null
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPointTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.licenses.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultOpenSourcesLicensesEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultOpenSourcesLicensesEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ DependenciesFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ )
+ }
+ val result = entryPoint.createNode(parentNode, BuildContext.root(null))
+ assertThat(result).isInstanceOf(DependenciesFlowNode::class.java)
+ }
+}
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
index d52300b5467..abc827d4e21 100644
--- a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
index 6fba7ba6510..ebbad0e16b0 100644
--- a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/licenses/test/build.gradle.kts b/features/licenses/test/build.gradle.kts
new file mode 100644
index 00000000000..d9a2870aee0
--- /dev/null
+++ b/features/licenses/test/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.features.licenses.test"
+}
+
+dependencies {
+ implementation(projects.features.licenses.api)
+ implementation(projects.libraries.architecture)
+ implementation(projects.tests.testutils)
+}
diff --git a/features/licenses/test/src/main/kotlin/io/element/android/features/licenses/test/FakeOpenSourceLicensesEntryPoint.kt b/features/licenses/test/src/main/kotlin/io/element/android/features/licenses/test/FakeOpenSourceLicensesEntryPoint.kt
new file mode 100644
index 00000000000..366aacbe259
--- /dev/null
+++ b/features/licenses/test/src/main/kotlin/io/element/android/features/licenses/test/FakeOpenSourceLicensesEntryPoint.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.licenses.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeOpenSourceLicensesEntryPoint : OpenSourceLicensesEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ ): Node {
+ lambdaError()
+ }
+}
diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts
index d2998871653..ab85e37594a 100644
--- a/features/location/api/build.gradle.kts
+++ b/features/location/api/build.gradle.kts
@@ -1,13 +1,15 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import config.BuildTimeConfig
import extension.buildConfigFieldStr
import extension.readLocalProperty
+import extension.testCommonDependencies
plugins {
id("io.element.android-compose-library")
@@ -65,10 +67,10 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.core)
+ implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(libs.coil.compose)
- testImplementation(libs.test.junit)
- testImplementation(libs.test.truth)
+ testCommonDependencies(libs)
}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt
index 0d720877a15..75938672075 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt
index 847bdf39217..22049c9548e 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
index c611340aea0..48816b2bb45 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt
@@ -1,17 +1,27 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.location.api
-import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.timeline.Timeline
/**
* The "Send location" screen.
*
* Allows a user to share a location message within a room.
*/
-interface SendLocationEntryPoint : SimpleFeatureEntryPoint
+interface SendLocationEntryPoint : FeatureEntryPoint {
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ timelineMode: Timeline.Mode,
+ ): Node
+}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationEntryPoint.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationEntryPoint.kt
index 7ae28a1e92b..03693a7d687 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationEntryPoint.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +14,14 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
interface ShowLocationEntryPoint : FeatureEntryPoint {
- data class Inputs(val location: Location, val description: String?) : NodeInputs
+ data class Inputs(
+ val location: Location,
+ val description: String?,
+ ) : NodeInputs
- fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: Inputs,
+ ): Node
}
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
index 382a65ae473..c58cadd66b6 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt
index eb8d0ea1b77..839cda02379 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt
index db75b5e30a5..d119dc7eb23 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt
index 40112cea4f7..48c78efc3c1 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
index 6ea4e9f9dea..81b80c8dc32 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt
index 533ecf8d2d3..6636976828a 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt
index 051b2448d44..17b620eec9d 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt
index 6718f6614fc..d1ad40176ea 100644
--- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt
+++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt
index ba44426a701..65c0acd88d4 100644
--- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt
+++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt
index dc53765836a..d5c6521258b 100644
--- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt
+++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts
index 4c62bdff1a4..bd41c7ecdf4 100644
--- a/features/location/impl/build.gradle.kts
+++ b/features/location/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -20,7 +22,7 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.location.api)
@@ -37,19 +39,10 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(libs.accompanist.permission)
implementation(projects.libraries.uiStrings)
- implementation(libs.dagger)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.testtags)
testImplementation(projects.services.analytics.test)
testImplementation(projects.features.messages.test)
- testImplementation(projects.tests.testutils)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/location/impl/src/main/AndroidManifest.xml b/features/location/impl/src/main/AndroidManifest.xml
index 662651e5702..ae728c09e15 100644
--- a/features/location/impl/src/main/AndroidManifest.xml
+++ b/features/location/impl/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt
index e662ef81150..9f2c3fa70b5 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt
@@ -1,20 +1,20 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.location.impl
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.location.api.BuildConfig
import io.element.android.features.location.api.LocationService
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultLocationService @Inject constructor() : LocationService {
+class DefaultLocationService : LocationService {
override fun isServiceAvailable(): Boolean {
return BuildConfig.MAPTILER_API_KEY.isNotEmpty()
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt
index 153adad3436..d01f3e7dd2b 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt
index 5b925e64ae3..6817f579e52 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt
index 8b5d3f06a33..7aef07e32b1 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt
index 21cc8df7bdd..0284c25a0ab 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,18 +13,17 @@ import android.content.Intent
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.location.api.Location
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.core.extensions.runCatchingExceptions
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber
import java.util.Locale
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class AndroidLocationActions @Inject constructor(
+class AndroidLocationActions(
@ApplicationContext private val context: Context
) : LocationActions {
override fun share(location: Location, label: String?) {
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt
index d4edeb8abb1..cd9efbd261e 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt
index fe6c24f6a72..1aa2e1269dd 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,14 +12,15 @@ import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
-import com.squareup.anvil.annotations.ContributesBinding
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import io.element.android.libraries.di.AppScope
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
+import dev.zacsweers.metro.ContributesBinding
@Suppress("unused")
-class DefaultPermissionsPresenter @AssistedInject constructor(
+@AssistedInject
+class DefaultPermissionsPresenter(
@Assisted private val permissions: List
) : PermissionsPresenter {
@AssistedFactory
@@ -32,7 +34,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
override fun present(): PermissionsState {
val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions)
- fun handleEvents(event: PermissionsEvents) {
+ fun handleEvent(event: PermissionsEvents) {
when (event) {
PermissionsEvents.RequestPermissions -> multiplePermissionsState.launchMultiplePermissionRequest()
}
@@ -45,7 +47,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
else -> PermissionsState.Permissions.NoneGranted
},
shouldShowRationale = multiplePermissionsState.shouldShowRationale,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt
index 951742aee44..a4b5ef12284 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt
index 410214ec88d..e7062264c9d 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +11,7 @@ package io.element.android.features.location.impl.common.permissions
import io.element.android.libraries.architecture.Presenter
interface PermissionsPresenter : Presenter {
- interface Factory {
+ fun interface Factory {
fun create(permissions: List): PermissionsPresenter
}
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt
index 1be342297b8..91191280d07 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationFloatingActionButton.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationFloatingActionButton.kt
new file mode 100644
index 00000000000..773544375f3
--- /dev/null
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationFloatingActionButton.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.impl.common.ui
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.ui.strings.CommonStrings
+
+/**
+ * Ref: See design in https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=3426-141111
+ */
+@Composable
+internal fun LocationFloatingActionButton(
+ isMapCenteredOnUser: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ FloatingActionButton(
+ shape = FloatingActionButtonDefaults.smallShape,
+ containerColor = ElementTheme.colors.bgCanvasDefault,
+ contentColor = ElementTheme.colors.iconPrimary,
+ onClick = onClick,
+ modifier = modifier
+ // Note: design is 40dp, but min is 48 for accessibility.
+ .size(48.dp),
+ ) {
+ val iconImage = if (isMapCenteredOnUser) {
+ CompoundIcons.LocationNavigatorCentred()
+ } else {
+ CompoundIcons.LocationNavigator()
+ }
+ Icon(
+ imageVector = iconImage,
+ contentDescription = stringResource(CommonStrings.a11y_move_the_map_to_my_location),
+ )
+ }
+}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
index e4c7b14768c..56399f7e9d6 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,16 +10,22 @@ package io.element.android.features.location.impl.send
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
+import io.element.android.libraries.matrix.api.timeline.Timeline
@ContributesBinding(AppScope::class)
-class DefaultSendLocationEntryPoint @Inject constructor() : SendLocationEntryPoint {
+class DefaultSendLocationEntryPoint : SendLocationEntryPoint {
override fun createNode(
parentNode: Node,
- buildContext: BuildContext
- ): SendLocationNode = parentNode.createNode(buildContext)
+ buildContext: BuildContext,
+ timelineMode: Timeline.Mode,
+ ): Node {
+ return parentNode.createNode(
+ buildContext = buildContext,
+ plugins = listOf(SendLocationNode.Inputs(timelineMode))
+ )
+ }
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt
index d8ffb3eff49..0d266eefc76 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt
index 4fd438c96e8..2184b52b441 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,20 +14,30 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-class SendLocationNode @AssistedInject constructor(
+@AssistedInject
+class SendLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: SendLocationPresenter,
+ presenterFactory: SendLocationPresenter.Factory,
analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
+ data class Inputs(
+ val timelineMode: Timeline.Mode,
+ ) : NodeInputs
+
+ private val presenter = presenterFactory.create(inputs().timelineMode)
+
init {
lifecycle.subscribe(
onResume = {
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt
index 4710e4bd50d..b753820e557 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,6 +16,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
@@ -23,25 +27,31 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
-import io.element.android.libraries.matrix.api.room.message.ReplyParameters
-import io.element.android.libraries.matrix.api.room.message.replyInThread
-import io.element.android.libraries.matrix.ui.messages.reply.eventId
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class SendLocationPresenter @Inject constructor(
+@AssistedInject
+class SendLocationPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val room: JoinedRoom,
+ @Assisted private val timelineMode: Timeline.Mode,
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContext,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
) : Presenter {
+ @AssistedFactory
+ fun interface Factory {
+ fun create(timelineMode: Timeline.Mode): SendLocationPresenter
+ }
+
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
@Composable
@@ -69,7 +79,7 @@ class SendLocationPresenter @Inject constructor(
}
}
- fun handleEvents(event: SendLocationEvents) {
+ fun handleEvent(event: SendLocationEvents) {
when (event) {
is SendLocationEvents.SendLocation -> scope.launch {
sendLocation(event, mode)
@@ -94,7 +104,7 @@ class SendLocationPresenter @Inject constructor(
mode = mode,
hasLocationPermission = permissionsState.isAnyGranted,
appName = appName,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
@@ -103,28 +113,20 @@ class SendLocationPresenter @Inject constructor(
mode: SendLocationState.Mode,
) {
val replyMode = messageComposerContext.composerMode as? MessageComposerMode.Reply
- val replyParams = replyMode?.replyToDetails?.let { details ->
- if (replyMode.inThread) {
- replyInThread(details.eventId())
- } else {
- ReplyParameters(
- inReplyToEventId = details.eventId(),
- enforceThreadReply = false,
- replyWithinThread = false
- )
- }
- }
+ val inReplyToEventId = replyMode?.eventId
when (mode) {
SendLocationState.Mode.PinLocation -> {
val geoUri = event.cameraPosition.toGeoUri()
- room.liveTimeline.sendLocation(
- body = generateBody(geoUri),
- geoUri = geoUri,
- description = null,
- zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
- assetType = AssetType.PIN,
- replyParameters = replyParams,
- )
+ getTimeline().flatMap {
+ it.sendLocation(
+ body = generateBody(geoUri),
+ geoUri = geoUri,
+ description = null,
+ zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
+ assetType = AssetType.PIN,
+ inReplyToEventId = inReplyToEventId,
+ )
+ }
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
@@ -136,14 +138,16 @@ class SendLocationPresenter @Inject constructor(
}
SendLocationState.Mode.SenderLocation -> {
val geoUri = event.toGeoUri()
- room.liveTimeline.sendLocation(
- body = generateBody(geoUri),
- geoUri = geoUri,
- description = null,
- zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
- assetType = AssetType.SENDER,
- replyParameters = replyParams,
- )
+ getTimeline().flatMap {
+ it.sendLocation(
+ body = generateBody(geoUri),
+ geoUri = geoUri,
+ description = null,
+ zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
+ assetType = AssetType.SENDER,
+ inReplyToEventId = inReplyToEventId,
+ )
+ }
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
@@ -155,6 +159,13 @@ class SendLocationPresenter @Inject constructor(
}
}
}
+
+ private suspend fun getTimeline(): Result {
+ return when (timelineMode) {
+ is Timeline.Mode.Thread -> room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
+ else -> Result.success(room.liveTimeline)
+ }
+ }
}
private fun SendLocationEvents.SendLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri()
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt
index fc2f1e91a81..4ca84c47a57 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt
index 858098dee95..238201cbecb 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
index 32df5af6c50..452a4aa8ae4 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -30,7 +31,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberTileStyleUrl
@@ -38,11 +38,11 @@ import io.element.android.features.location.impl.R
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
+import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
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.components.BottomSheetScaffold
-import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@@ -189,17 +189,13 @@ fun SendLocationView(
tint = Color.Unspecified,
modifier = Modifier.centerBottomEdge(this),
)
- FloatingActionButton(
+ LocationFloatingActionButton(
+ isMapCenteredOnUser = state.mode == SendLocationState.Mode.SenderLocation,
onClick = { state.eventSink(SendLocationEvents.SwitchToMyLocationMode) },
modifier = Modifier
.align(Alignment.BottomEnd)
- .padding(end = 16.dp, bottom = 72.dp + navBarPadding),
- ) {
- when (state.mode) {
- SendLocationState.Mode.PinLocation -> Icon(imageVector = CompoundIcons.LocationNavigator(), contentDescription = null)
- SendLocationState.Mode.SenderLocation -> Icon(imageVector = CompoundIcons.LocationNavigatorCentred(), contentDescription = null)
- }
- }
+ .padding(end = 18.dp, bottom = 72.dp + navBarPadding),
+ )
}
}
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt
index ea45ef2690e..7d1f5f1ef47 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,15 +10,18 @@ package io.element.android.features.location.impl.show
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultShowLocationEntryPoint @Inject constructor() : ShowLocationEntryPoint {
- override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: ShowLocationEntryPoint.Inputs): Node {
+class DefaultShowLocationEntryPoint : ShowLocationEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: ShowLocationEntryPoint.Inputs,
+ ): Node {
return parentNode.createNode(buildContext, listOf(inputs))
}
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt
index a1464aeaf99..12f368fa113 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt
index 1a74130eb07..86d77417526 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,21 +14,22 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.anvilannotations.ContributesNode
+import io.element.android.annotations.ContributesNode
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
-class ShowLocationNode @AssistedInject constructor(
- presenterFactory: ShowLocationPresenter.Factory,
- analyticsService: AnalyticsService,
+@AssistedInject
+class ShowLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
+ presenterFactory: ShowLocationPresenter.Factory,
+ analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
init {
lifecycle.subscribe(
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt
index 152a201ad23..3dcccef8861 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,9 +15,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedFactory
+import dev.zacsweers.metro.AssistedInject
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
@@ -26,15 +27,16 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
-class ShowLocationPresenter @AssistedInject constructor(
+@AssistedInject
+class ShowLocationPresenter(
+ @Assisted private val location: Location,
+ @Assisted private val description: String?,
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
- @Assisted private val location: Location,
- @Assisted private val description: String?
) : Presenter {
@AssistedFactory
- interface Factory {
+ fun interface Factory {
fun create(location: Location, description: String?): ShowLocationPresenter
}
@@ -55,7 +57,7 @@ class ShowLocationPresenter @AssistedInject constructor(
}
}
- fun handleEvents(event: ShowLocationEvents) {
+ fun handleEvent(event: ShowLocationEvents) {
when (event) {
ShowLocationEvents.Share -> locationActions.share(location, description)
is ShowLocationEvents.TrackMyLocation -> {
@@ -85,7 +87,7 @@ class ShowLocationPresenter @AssistedInject constructor(
hasLocationPermission = permissionsState.isAnyGranted,
isTrackMyLocation = isTrackMyLocation,
appName = appName,
- eventSink = ::handleEvents,
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt
index cee52c126b5..96635d6df85 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt
index 98b700356bc..7d03a1ebb22 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
index 03d2a3c714b..6bc9e27bc67 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -27,10 +28,10 @@ import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
+import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
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.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
@@ -118,14 +119,10 @@ fun ShowLocationView(
)
},
floatingActionButton = {
- FloatingActionButton(
+ LocationFloatingActionButton(
+ isMapCenteredOnUser = state.isTrackMyLocation,
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
- ) {
- when (state.isTrackMyLocation) {
- false -> Icon(imageVector = CompoundIcons.LocationNavigator(), contentDescription = null)
- true -> Icon(imageVector = CompoundIcons.LocationNavigatorCentred(), contentDescription = null)
- }
- }
+ )
},
) { paddingValues ->
Column(
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt
index fd687dd938a..b0e899730b6 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt
index 01f4f522f64..a4352444dc0 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt
index 5b584a39f88..f82635e562c 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -67,6 +68,7 @@ internal class AndroidLocationActionsTest {
accuracy = 0f
)
// Set a locale with comma as decimal separator
+ @Suppress("DEPRECATION")
Locale.setDefault(Locale.Category.FORMAT, Locale("pt", "BR"))
val actual = buildUrl(location, "(weird/stuff here)", ::urlEncoder)
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt
index 488408eaf25..94dc9722139 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt
index 7351dd71f43..94d909a7efc 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -19,7 +20,7 @@ class FakePermissionsPresenter : PermissionsPresenter {
private var state = PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
- eventSink = ::handleEvent
+ eventSink = ::handleEvent,
)
set(value) {
field = value.copy(eventSink = ::handleEvent)
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.kt
new file mode 100644
index 00000000000..a90afd5a9d4
--- /dev/null
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPointTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.impl.send
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.location.impl.common.actions.FakeLocationActions
+import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
+import io.element.android.features.messages.test.FakeMessageComposerContext
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.test.core.aBuildMeta
+import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultSendLocationEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultSendLocationEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ SendLocationNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenterFactory = { timelineMode: Timeline.Mode ->
+ SendLocationPresenter(
+ permissionsPresenterFactory = { FakePermissionsPresenter() },
+ room = FakeJoinedRoom(),
+ timelineMode = timelineMode,
+ analyticsService = FakeAnalyticsService(),
+ messageComposerContext = FakeMessageComposerContext(),
+ locationActions = FakeLocationActions(),
+ buildMeta = aBuildMeta(),
+ )
+ },
+ analyticsService = FakeAnalyticsService(),
+ )
+ }
+ val timelineMode = Timeline.Mode.Live
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ timelineMode = timelineMode,
+ )
+ assertThat(result).isInstanceOf(SendLocationNode::class.java)
+ assertThat(result.plugins).contains(SendLocationNode.Inputs(timelineMode))
+ }
+}
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
index 7addee38e4a..23ab3847cdf 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -20,9 +21,10 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.test.FakeMessageComposerContext
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
-import io.element.android.libraries.matrix.api.room.message.ReplyParameters
+import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -55,6 +57,7 @@ class SendLocationPresenterTest {
override fun create(permissions: List): PermissionsPresenter = fakePermissionsPresenter
},
room = joinedRoom,
+ timelineMode = Timeline.Mode.Live,
analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext,
locationActions = fakeLocationActions,
@@ -264,7 +267,7 @@ class SendLocationPresenterTest {
@Test
fun `share sender location`() = runTest {
- val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
+ val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
@@ -328,7 +331,7 @@ class SendLocationPresenterTest {
@Test
fun `share pin location`() = runTest {
- val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
+ val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
@@ -392,7 +395,7 @@ class SendLocationPresenterTest {
@Test
fun `composer context passes through analytics`() = runTest {
- val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
+ val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt
new file mode 100644
index 00000000000..a49b887a421
--- /dev/null
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.impl.show
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.location.api.Location
+import io.element.android.features.location.api.ShowLocationEntryPoint
+import io.element.android.features.location.impl.common.actions.FakeLocationActions
+import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
+import io.element.android.libraries.matrix.test.core.aBuildMeta
+import io.element.android.services.analytics.test.FakeAnalyticsService
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultShowLocationEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Test
+ fun `test node builder`() {
+ val entryPoint = DefaultShowLocationEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ ShowLocationNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ presenterFactory = { location: Location, description: String? ->
+ ShowLocationPresenter(
+ permissionsPresenterFactory = { FakePermissionsPresenter() },
+ locationActions = FakeLocationActions(),
+ buildMeta = aBuildMeta(),
+ location = location,
+ description = description,
+ )
+ },
+ analyticsService = FakeAnalyticsService(),
+ )
+ }
+ val inputs = ShowLocationEntryPoint.Inputs(
+ location = Location(37.4219983, -122.084, 10f),
+ description = "My location",
+ )
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ inputs = inputs,
+ )
+ assertThat(result).isInstanceOf(ShowLocationNode::class.java)
+ assertThat(result.plugins).contains(inputs)
+ }
+}
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt
index 937d1d475e9..df22863c7c6 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -37,10 +38,10 @@ class ShowLocationPresenterTest {
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List): PermissionsPresenter = fakePermissionsPresenter
},
- fakeLocationActions,
- fakeBuildMeta,
- location,
- A_DESCRIPTION,
+ locationActions = fakeLocationActions,
+ buildMeta = fakeBuildMeta,
+ location = location,
+ description = A_DESCRIPTION,
)
@Test
diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt
index 2125fff4db6..2245360bb25 100644
--- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt
+++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/location/test/build.gradle.kts b/features/location/test/build.gradle.kts
index 024ae2c3037..f84e8ba7726 100644
--- a/features/location/test/build.gradle.kts
+++ b/features/location/test/build.gradle.kts
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,5 +15,8 @@ android {
}
dependencies {
- implementation(projects.features.location.api)
+ api(projects.features.location.api)
+ implementation(projects.libraries.matrix.api)
+ implementation(libs.appyx.core)
+ implementation(projects.tests.testutils)
}
diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt
index 8ba29a65aa0..f961a81c9bb 100644
--- a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt
+++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +11,7 @@ package io.element.android.features.location.test
import io.element.android.features.location.api.LocationService
class FakeLocationService(
- private val isServiceAvailable: Boolean,
+ private val isServiceAvailable: Boolean = false,
) : LocationService {
override fun isServiceAvailable() = isServiceAvailable
}
diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeSendLocationEntryPoint.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeSendLocationEntryPoint.kt
new file mode 100644
index 00000000000..2a1741e6c8e
--- /dev/null
+++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeSendLocationEntryPoint.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.location.api.SendLocationEntryPoint
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeSendLocationEntryPoint : SendLocationEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ timelineMode: Timeline.Mode,
+ ): Node = lambdaError()
+}
diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeShowLocationEntryPoint.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeShowLocationEntryPoint.kt
new file mode 100644
index 00000000000..b6a94091d93
--- /dev/null
+++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeShowLocationEntryPoint.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.test
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.location.api.ShowLocationEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeShowLocationEntryPoint : ShowLocationEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: ShowLocationEntryPoint.Inputs,
+ ): Node = lambdaError()
+}
diff --git a/features/lockscreen/api/build.gradle.kts b/features/lockscreen/api/build.gradle.kts
index 4419b7a9faf..a09c1a4d405 100644
--- a/features/lockscreen/api/build.gradle.kts
+++ b/features/lockscreen/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt
index e9ba05ba74c..aa891ada4f2 100644
--- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt
+++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,13 +16,14 @@ import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
interface LockScreenEntryPoint : FeatureEntryPoint {
- fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder
- fun pinUnlockIntent(context: Context): Intent
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ navTarget: Target,
+ callback: Callback,
+ ): Node
- interface NodeBuilder {
- fun callback(callback: Callback): NodeBuilder
- fun build(): Node
- }
+ fun pinUnlockIntent(context: Context): Intent
interface Callback : Plugin {
fun onSetupDone()
diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt
index 5650122ee0c..1a63ebc6409 100644
--- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt
+++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt
index 4919442e28f..bd644e69e92 100644
--- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt
+++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -35,12 +36,6 @@ interface LockScreenService {
fun isPinSetup(): Flow
}
-/**
- * Check if the app is currently locked.
- */
-val LockScreenService.isLocked: Boolean
- get() = lockState.value == LockScreenLockState.Locked
-
/**
* Makes sure the secure flag is set on the activity if the pin is setup.
* @param activity the activity to set the flag on.
diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts
index ad77b20e600..63aec590deb 100644
--- a/features/lockscreen/impl/build.gradle.kts
+++ b/features/lockscreen/impl/build.gradle.kts
@@ -1,9 +1,11 @@
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -20,13 +22,14 @@ android {
}
}
-setupAnvil()
+setupDependencyInjection()
dependencies {
api(projects.features.lockscreen.api)
implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.core)
+ implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
@@ -37,27 +40,19 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiUtils)
implementation(projects.features.logout.api)
+ implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.biometric)
- testImplementation(libs.test.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testImplementation(libs.androidx.test.ext.junit)
+ testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
- testImplementation(projects.tests.testutils)
testImplementation(projects.libraries.cryptography.test)
testImplementation(projects.libraries.cryptography.impl)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.features.logout.test)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/lockscreen/impl/src/main/AndroidManifest.xml b/features/lockscreen/impl/src/main/AndroidManifest.xml
index 2b4ccb4296a..13b1262f27d 100644
--- a/features/lockscreen/impl/src/main/AndroidManifest.xml
+++ b/features/lockscreen/impl/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt
index 907ed9fa849..ec8fdc6d8b6 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,35 +12,32 @@ import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
- val callbacks = mutableListOf()
-
- return object : LockScreenEntryPoint.NodeBuilder {
- override fun callback(callback: LockScreenEntryPoint.Callback): LockScreenEntryPoint.NodeBuilder {
- callbacks += callback
- return this
- }
-
- override fun build(): Node {
- val inputs = LockScreenFlowNode.Inputs(
+class DefaultLockScreenEntryPoint : LockScreenEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ navTarget: LockScreenEntryPoint.Target,
+ callback: LockScreenEntryPoint.Callback,
+ ): Node {
+ return parentNode.createNode(
+ buildContext = buildContext,
+ plugins = listOf(
+ LockScreenFlowNode.Inputs(
when (navTarget) {
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
}
- )
- val plugins = listOf(inputs) + callbacks
- return parentNode.createNode(buildContext, plugins)
- }
- }
+ ),
+ callback,
+ )
+ )
}
override fun pinUnlockIntent(context: Context): Intent {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
index 85ec2e31307..7ac42feda68 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt
@@ -1,13 +1,16 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
@@ -15,11 +18,7 @@ import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnl
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
-import io.element.android.libraries.featureflag.api.FeatureFlagService
-import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.services.appnavstate.api.AppForegroundStateService
@@ -29,18 +28,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
-import javax.inject.Inject
import kotlin.time.Duration
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class DefaultLockScreenService @Inject constructor(
+class DefaultLockScreenService(
private val lockScreenConfig: LockScreenConfig,
- private val featureFlagService: FeatureFlagService,
private val lockScreenStore: LockScreenStore,
private val pinCodeManager: PinCodeManager,
@AppCoroutineScope
@@ -78,15 +74,14 @@ class DefaultLockScreenService @Inject constructor(
}
/**
- * Makes sure to delete the pin code when the session is deleted.
+ * Makes sure to delete the pin code when the last session is deleted.
*/
private fun observeSessionsState() {
sessionObserver.addListener(object : SessionListener {
- override suspend fun onSessionCreated(userId: String) = Unit
-
- override suspend fun onSessionDeleted(userId: String) {
- // TODO handle multi session at some point
- pinCodeManager.deletePinCode()
+ override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
+ if (wasLastSession) {
+ pinCodeManager.deletePinCode()
+ }
}
})
}
@@ -108,12 +103,7 @@ class DefaultLockScreenService @Inject constructor(
}
override fun isPinSetup(): Flow {
- return combine(
- featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinUnlock),
- pinCodeManager.hasPinCode()
- ) { isEnabled, hasPinCode ->
- isEnabled && hasPinCode
- }
+ return pinCodeManager.hasPinCode()
}
override fun isSetupRequired(): Flow {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt
index de20b6f09a9..c246390a635 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt
@@ -1,16 +1,17 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Module
-import dagger.Provides
-import io.element.android.libraries.di.AppScope
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.Provides
import kotlin.time.Duration
import io.element.android.appconfig.LockScreenConfig as AppConfigLockScreenConfig
@@ -25,7 +26,7 @@ data class LockScreenConfig(
)
@ContributesTo(AppScope::class)
-@Module
+@BindingContainer
object LockScreenConfigModule {
@Provides
fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig(
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
index 56eaeb54726..2a84ddc0aa8 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,9 +16,9 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
@@ -29,7 +30,8 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class LockScreenFlowNode @AssistedInject constructor(
+@AssistedInject
+class LockScreenFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
) : BaseFlowNode(
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt
index 7358cf248e5..a96c713ff26 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt
index 887334812e5..99178457255 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt
index 91617b270a7..941256f8b47 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt
index a817faf3533..8bb044fd06c 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -22,27 +23,26 @@ import androidx.compose.ui.res.stringResource
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.LocalLifecycleOwner
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.R
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.SecretKeyRepository
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
+import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.concurrent.CopyOnWriteArrayList
-import javax.inject.Inject
private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_BIOMETRIC"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
-class DefaultBiometricAuthenticatorManager @Inject constructor(
+class DefaultBiometricAuthenticatorManager(
@ApplicationContext private val context: Context,
private val lockScreenStore: LockScreenStore,
private val lockScreenConfig: LockScreenConfig,
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
index 5e83ac9688a..9a7a0abd098 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt
index ed5bb2e4ac9..439c6aeaa7f 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
index 6dfd1e3e23d..1ebf00dd106 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
@@ -1,28 +1,28 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.EncryptionResult
import io.element.android.libraries.cryptography.api.SecretKeyRepository
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.Flow
import java.util.concurrent.CopyOnWriteArrayList
-import javax.inject.Inject
private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
-class DefaultPinCodeManager @Inject constructor(
+class DefaultPinCodeManager(
private val secretKeyRepository: SecretKeyRepository,
private val encryptionDecryptionService: EncryptionDecryptionService,
private val lockScreenStore: LockScreenStore,
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt
index 9daadaa5d72..c9c8b7bf4e8 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
index c36a976f25c..9282f3e7dfd 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt
index 0ca285321c9..64200d0da3a 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
index 78356fdddf4..f9cb2afcfe6 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt
@@ -1,14 +1,15 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.pin.model
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.toImmutableList
data class PinEntry(
val digits: ImmutableList,
@@ -17,7 +18,7 @@ data class PinEntry(
fun createEmpty(size: Int): PinEntry {
val digits = List(size) { PinDigit.Empty }
return PinEntry(
- digits = digits.toPersistentList()
+ digits = digits.toImmutableList()
)
}
}
@@ -37,7 +38,7 @@ data class PinEntry(
newDigits[index] = PinDigit.Filled(char)
}
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun deleteLast(): PinEntry {
@@ -46,7 +47,7 @@ data class PinEntry(
newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled ->
newDigits[lastFilled] = PinDigit.Empty
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun addDigit(digit: Char): PinEntry {
@@ -55,7 +56,7 @@ data class PinEntry(
newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty ->
newDigits[firstEmpty] = PinDigit.Filled(digit)
}
- return copy(digits = newDigits.toPersistentList())
+ return copy(digits = newDigits.toImmutableList())
}
fun clear(): PinEntry {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
index 19f729c94c2..2d62427e025 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
index 79a8c997c4c..0bd26729d54 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,14 +15,13 @@ import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.lifecycle.subscribe
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 com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode
@@ -30,18 +30,20 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.ui.common.nodes.emptyNode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class LockScreenSettingsFlowNode @AssistedInject constructor(
+@AssistedInject
+class LockScreenSettingsFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val pinCodeManager: PinCodeManager,
) : BaseFlowNode(
backstack = BackStack(
- initialElement = NavTarget.Unknown,
+ initialElement = NavTarget.Loading,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -49,7 +51,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
) {
sealed interface NavTarget : Parcelable {
@Parcelize
- data object Unknown : NavTarget
+ data object Loading : NavTarget
@Parcelize
data object Unlock : NavTarget
@@ -93,6 +95,9 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
+ NavTarget.Loading -> {
+ emptyNode(buildContext)
+ }
NavTarget.Unlock -> {
val callback = object : PinUnlockNode.Callback {
override fun onUnlock() {
@@ -106,13 +111,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
}
NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback {
- override fun onChangePinClick() {
+ override fun navigateToSetupPin() {
backstack.push(NavTarget.SetupPin)
}
}
createNode(buildContext, plugins = listOf(callback))
}
- NavTarget.Unknown -> node(buildContext) { }
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt
index 937927bb74d..e66bc13691f 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,25 +13,24 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class LockScreenSettingsNode @AssistedInject constructor(
+@AssistedInject
+class LockScreenSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: LockScreenSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
- fun onChangePinClick()
+ fun navigateToSetupPin()
}
- private fun onChangePinClick() {
- plugins().forEach { it.onChangePinClick() }
- }
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
@@ -38,7 +38,7 @@ class LockScreenSettingsNode @AssistedInject constructor(
LockScreenSettingsView(
state = state,
onBackClick = this::navigateUp,
- onChangePinClick = this::onChangePinClick,
+ onChangePinClick = callback::navigateToSetupPin,
modifier = modifier,
)
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
index 9f44b03a100..589794bde09 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
@@ -23,9 +25,9 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class LockScreenSettingsPresenter @Inject constructor(
+@Inject
+class LockScreenSettingsPresenter(
private val lockScreenConfig: LockScreenConfig,
private val pinCodeManager: PinCodeManager,
private val lockScreenStore: LockScreenStore,
@@ -49,7 +51,7 @@ class LockScreenSettingsPresenter @Inject constructor(
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
- fun handleEvents(event: LockScreenSettingsEvents) {
+ fun handleEvent(event: LockScreenSettingsEvents) {
when (event) {
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
LockScreenSettingsEvents.ConfirmRemovePin -> {
@@ -81,7 +83,7 @@ class LockScreenSettingsPresenter @Inject constructor(
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
showToggleBiometric = biometricAuthenticatorManager.isDeviceSecured,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
index f49f2c2b265..a69d6335089 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
index 5cf70687351..43f20c1c52b 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
index a843525d008..fe5f20da0d5 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt
index d6df41820f1..6f47395a268 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,12 +15,11 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
@@ -27,12 +27,14 @@ import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometri
import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
-class LockScreenSetupFlowNode @AssistedInject constructor(
+@AssistedInject
+class LockScreenSetupFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val pinCodeManager: PinCodeManager,
@@ -49,9 +51,7 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
fun onSetupDone()
}
- private fun onSetupDone() {
- plugins().forEach { it.onSetupDone() }
- }
+ private val callback: Callback = callback()
sealed interface NavTarget : Parcelable {
@Parcelize
@@ -66,7 +66,7 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
if (biometricAuthenticatorManager.hasAvailableAuthenticator) {
backstack.newRoot(NavTarget.Biometric)
} else {
- onSetupDone()
+ callback.onSetupDone()
}
}
}
@@ -90,7 +90,7 @@ class LockScreenSetupFlowNode @AssistedInject constructor(
NavTarget.Biometric -> {
val callback = object : SetupBiometricNode.Callback {
override fun onBiometricSetupDone() {
- onSetupDone()
+ callback.onSetupDone()
}
}
createNode(buildContext, plugins = listOf(callback))
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt
index 964d6b3d386..ab8b18642e5 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt
index eaa2189cc07..c74b9cd77f7 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,14 +14,15 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class SetupBiometricNode @AssistedInject constructor(
+@AssistedInject
+class SetupBiometricNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: SetupBiometricPresenter,
@@ -29,16 +31,14 @@ class SetupBiometricNode @AssistedInject constructor(
fun onBiometricSetupDone()
}
- private fun onSetupDone() {
- plugins().forEach { it.onBiometricSetupDone() }
- }
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LaunchedEffect(state.isBiometricSetupDone) {
if (state.isBiometricSetupDone) {
- onSetupDone()
+ callback.onBiometricSetupDone()
}
}
SetupBiometricView(
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt
index d10485112dd..3af2a28851f 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,14 +14,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class SetupBiometricPresenter @Inject constructor(
+@Inject
+class SetupBiometricPresenter(
private val lockScreenStore: LockScreenStore,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : Presenter {
@@ -33,7 +35,7 @@ class SetupBiometricPresenter @Inject constructor(
val coroutineScope = rememberCoroutineScope()
val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator()
- fun handleEvents(event: SetupBiometricEvents) {
+ fun handleEvent(event: SetupBiometricEvents) {
when (event) {
SetupBiometricEvents.AllowBiometric -> coroutineScope.launch {
biometricUnlock.setup()
@@ -51,7 +53,7 @@ class SetupBiometricPresenter @Inject constructor(
return SetupBiometricState(
isBiometricSetupDone = isBiometricSetupDone,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt
index afe73beee0a..2843c028d15 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt
index 6c30fc71723..d725d2de973 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
index 761d48ec381..35b1ec76c0a 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
index a111beb984e..276a94b2fcc 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
index 5a4309c7beb..2f86ca5ec7e 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,13 +13,14 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class SetupPinNode @AssistedInject constructor(
+@AssistedInject
+class SetupPinNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: SetupPinPresenter,
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
index b280c00957a..ac5b5bd1cc8 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,6 +14,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
@@ -21,14 +23,14 @@ import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPin
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import kotlinx.coroutines.delay
-import javax.inject.Inject
/**
* Some time for the ui to refresh before showing confirmation step.
*/
private const val DELAY_BEFORE_CONFIRMATION_STEP_IN_MILLIS = 100L
-class SetupPinPresenter @Inject constructor(
+@Inject
+class SetupPinPresenter(
private val lockScreenConfig: LockScreenConfig,
private val pinValidator: PinValidator,
private val buildMeta: BuildMeta,
@@ -72,7 +74,7 @@ class SetupPinPresenter @Inject constructor(
}
}
- fun handleEvents(event: SetupPinEvents) {
+ fun handleEvent(event: SetupPinEvents) {
when (event) {
is SetupPinEvents.OnPinEntryChanged -> {
// Use the fromConfirmationStep flag from ui to avoid race condition.
@@ -105,7 +107,7 @@ class SetupPinPresenter @Inject constructor(
isConfirmationStep = isConfirmationStep,
setupPinFailure = setupPinFailure,
appName = buildMeta.applicationName,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
index ee9801b3843..2d5124d4404 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
index 2c4e08c130a..f50643e367d 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
index 81bfa4c1a43..5f2320db32e 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
index 9716af0ac64..c84d892fbcf 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/PinValidator.kt
@@ -1,17 +1,19 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.setup.pin.validation
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
-import javax.inject.Inject
-class PinValidator @Inject constructor(private val lockScreenConfig: LockScreenConfig) {
+@Inject
+class PinValidator(private val lockScreenConfig: LockScreenConfig) {
sealed interface Result {
data object Valid : Result
data class Invalid(val failure: SetupPinFailure) : Result
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
index cedefa3f191..94e4aa5fb20 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/validation/SetupPinFailure.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
index d1ec05f643b..c4558812deb 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
index e4321f73585..d48a30e8150 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/LockScreenStore.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
index b3086ddb9ee..6b99d905928 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt
@@ -1,50 +1,45 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.storage
-import android.content.Context
-import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.lockscreen.impl.LockScreenConfig
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store")
-
-@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
-class PreferencesLockScreenStore @Inject constructor(
- @ApplicationContext private val context: Context,
+class PreferencesLockScreenStore(
+ preferenceDataStoreFactory: PreferenceDataStoreFactory,
private val lockScreenConfig: LockScreenConfig,
) : LockScreenStore {
+ private val dataStore = preferenceDataStoreFactory.create("pin_code_store")
+
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences.getRemainingPinCodeAttemptsNumber()
}.first()
}
override suspend fun onWrongPin() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
val current = preferences.getRemainingPinCodeAttemptsNumber()
val remaining = (current - 1).coerceAtLeast(0)
preferences[remainingAttemptsKey] = remaining
@@ -52,43 +47,43 @@ class PreferencesLockScreenStore @Inject constructor(
}
override suspend fun resetCounter() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
}
}
override suspend fun getEncryptedCode(): String? {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[pinCodeKey]
}.first()
}
override suspend fun saveEncryptedPinCode(pinCode: String) {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[pinCodeKey] = pinCode
}
}
override suspend fun deleteEncryptedPinCode() {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences.remove(pinCodeKey)
}
}
override fun hasPinCode(): Flow {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[pinCodeKey] != null
}
}
override fun isBiometricUnlockAllowed(): Flow {
- return context.dataStore.data.map { preferences ->
+ return dataStore.data.map { preferences ->
preferences[biometricUnlockKey] ?: false
}
}
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
- context.dataStore.edit { preferences ->
+ dataStore.edit { preferences ->
preferences[biometricUnlockKey] = isAllowed
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
index e72d05fa272..bd9043859f0 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt
index 0f3da02a7e6..2a475814c9e 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -11,13 +12,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
-import javax.inject.Inject
-class PinUnlockHelper @Inject constructor(
+@Inject
+class PinUnlockHelper(
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val pinCodeManager: PinCodeManager
) {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt
index cea53bb844d..459e165e001 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,14 +14,15 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
-class PinUnlockNode @AssistedInject constructor(
+@AssistedInject
+class PinUnlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val presenter: PinUnlockPresenter,
@@ -29,18 +31,14 @@ class PinUnlockNode @AssistedInject constructor(
fun onUnlock()
}
- private fun onUnlock() {
- plugins().forEach {
- it.onUnlock()
- }
- }
+ private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LaunchedEffect(state.isUnlocked) {
if (state.isUnlocked) {
- onUnlock()
+ callback.onUnlock()
}
}
PinUnlockView(
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
index 707ca6b710e..5429320fc7b 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
@@ -29,9 +31,9 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.annotations.AppCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class PinUnlockPresenter @Inject constructor(
+@Inject
+class PinUnlockPresenter(
private val pinCodeManager: PinCodeManager,
private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
private val logoutUseCase: LogoutUseCase,
@@ -93,7 +95,7 @@ class PinUnlockPresenter @Inject constructor(
isUnlocked.value = true
}
- fun handleEvents(event: PinUnlockEvents) {
+ fun handleEvent(event: PinUnlockEvents) {
when (event) {
is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntryState.value = pinEntry.process(event.pinKeypadModel)
@@ -128,7 +130,7 @@ class PinUnlockPresenter @Inject constructor(
showBiometricUnlock = biometricUnlock.isActive,
biometricUnlockResult = biometricUnlockResult,
isUnlocked = isUnlocked.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
@@ -173,7 +175,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch {
suspend {
- logoutUseCase.logout(ignoreSdkError = true)
+ logoutUseCase.logoutAll(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
index dd24fbefd76..2bbcbe335c9 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
index 54647eaad87..2beb8babe3b 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
index baa856307c4..659f8c29668 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt
index dd273ebac1c..6209c19be27 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,7 +15,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.lifecycle.lifecycleScope
+import dev.zacsweers.metro.Inject
+import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
@@ -26,7 +32,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.launch
-import javax.inject.Inject
class PinUnlockActivity : AppCompatActivity() {
internal companion object {
@@ -46,9 +51,13 @@ class PinUnlockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
bindings().inject(this)
setContent {
+ val colors by remember {
+ enterpriseService.semanticColorsFlow(sessionId = null)
+ }.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
- enterpriseService = enterpriseService,
+ compoundLight = colors.light,
+ compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val state = presenter.present()
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt
index 9f538dfd3f4..c8b65afbe03 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt
@@ -1,15 +1,16 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl.unlock.di
-import com.squareup.anvil.annotations.ContributesTo
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
-import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface PinUnlockBindings {
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt
index 820b5574230..2131853e3ee 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt
index 9f54e6bd5ef..36c783951ef 100644
--- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt
+++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/main/res/values-bg/translations.xml b/features/lockscreen/impl/src/main/res/values-bg/translations.xml
index af8e8a9853b..7bd2895990f 100644
--- a/features/lockscreen/impl/src/main/res/values-bg/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-bg/translations.xml
@@ -1,7 +1,12 @@
+ "биометрично удостоверяване"
+ "биометрично отключване"
+ "Отключване с биометрия"
+ "Потвърдете биометричните данни""Забравихте PIN?""Промяна на PIN кода"
+ "Разрешаване на биометрично отключване""Премахване на PIN""Сигурни ли сте, че искате да премахнете PIN?""Премахване на PIN?"
@@ -9,6 +14,10 @@
"Предпочитам да използвам PIN""Избор на PIN""Потвърждаване на PIN"
+ "Заключете %1$s, за да добавите допълнителна сигурност към вашите чатове.
+
+Изберете нещо запомнящо се. Ако забравите този PIN, ще бъдете излезли от приложението."
+ "Не можете да изберете това за ваш PIN код от съображения за сигурност""Избор на различен PIN""Моля, въведете един и същ PIN два пъти""PINs не съвпадат"
@@ -20,6 +29,7 @@
"Грешен PIN. Имате още %1$d шанс""Грешен PIN. Имате още %1$d шанса"
+ "Използване на биометрия""Използване на PIN""Излизане…"
diff --git a/features/lockscreen/impl/src/main/res/values-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml
index f7e06457b41..dd74818610f 100644
--- a/features/lockscreen/impl/src/main/res/values-de/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml
@@ -8,29 +8,29 @@
"PIN-Code ändern""Biometrisches Entsperren zulassen""Pin entfernen"
- "Sind Sie sicher, dass Sie die PIN entfernen wollen?"
+ "Bist du sicher, dass du die PIN entfernen willst?""PIN entfernen?""%1$s zulassen""Ich möchte diese PIN verwenden.""Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren""PIN wählen""PIN bestätigen"
- "Erhöhen Sie die Sicherheit von %1$s mit einem PIN Code.
+ "Sperre %1$s um deine Chats zusätzlich abzusichern.
-Wählen Sie etwas Einprägsames. Wenn Sie die PIN vergessen, werden Sie aus der App ausgeloggt."
+Wähle eine einprägsame PIN. Wenn du sie vergisst, wirst du aus der App abgemeldet.""Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden.""Bitte eine andere PIN verwenden.""Bitte gib die gleiche PIN wie zuvor ein.""Die PINs stimmen nicht überein"
- "Um fortzufahren, müssen Sie sich erneut anmelden und eine neue PIN erstellen"
- "Sie werden abgemeldet"
+ "Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen"
+ "Du wirst abgemeldet"
- "Sie haben %1$d Entsperrversuch"
- "Sie haben %1$d Entsperrversuche"
+ "Du hast %1$d Versuch, um zu entsperren"
+ "Du hast %1$d Versuche, um zu entsperren"
- "Falsche PIN. Sie haben %1$d weiteren Versuch"
- "Falsche PIN. Sie haben %1$d weitere Versuche"
+ "Falsche PIN. Du hast %1$d weiteren Versuch"
+ "Falsche PIN. Du hast %1$d weitere Versuche""Biometrie verwenden""PIN verwenden"
diff --git a/features/lockscreen/impl/src/main/res/values-fa/translations.xml b/features/lockscreen/impl/src/main/res/values-fa/translations.xml
index 2d224ea3025..8e57ec8d684 100644
--- a/features/lockscreen/impl/src/main/res/values-fa/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-fa/translations.xml
@@ -12,10 +12,10 @@
"برداشتن پین؟""اجازه به %1$s""ترجیح میدهم از پین استفاده کنم"
- "زمیانتان را ذخیره کرده و از %1$s برای قفلگشایی هربارهٔ کاره استفاده کنید"
+ "زمانتان را ذخیره کرده و از %1$s برای قفلگشایی هربارهٔ کاره استفاده کنید""گزینش پین""تأیید پین"
- "قفل %1$s برای افزودن امنیت بیشتر به گفتگوهایتان.
+ "قفل کردن %1$s برای افزودن امنیت بیشتر به گفتگوهایتان.
چیزی به یاد ماندنی انتخاب کنید. اگر این پین را فراموش کنید، از برنامه خارج خواهید شد.""به دلیل امنیتی نمیتوانید این پین را برگزینید"
@@ -24,14 +24,6 @@
"پینها مطابق نیستند""برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید""دارید خارج میشوید"
-
- "شما %1$d تلاش برای باز کردن قفل دارید"
- "شما %1$d تلاش برای باز کردن قفل دارید"
-
-
- "پین اشتباه است. شما %1$d شانس دیگر دارید"
- "پین اشتباه است. شما %1$d شانس دیگر دارید"
- "استفاده از زیستسنجی""استفاده از پین""خارج شدن…"
diff --git a/features/lockscreen/impl/src/main/res/values-fi/translations.xml b/features/lockscreen/impl/src/main/res/values-fi/translations.xml
index ae2abef6e83..02df7528e59 100644
--- a/features/lockscreen/impl/src/main/res/values-fi/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-fi/translations.xml
@@ -9,7 +9,7 @@
"Salli biometrinen tunnistus""Poista PIN-koodi""Haluatko varmasti poistaa PIN-koodin?"
- "Poista PIN-koodi?"
+ "Poistetaanko PIN-koodi?""Salli %1$s""Käytän mieluummin PIN-koodia""Säästä aikaa ja ota käyttöön %1$s"
diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml
index 5f6fa68ff67..514d2461a26 100644
--- a/features/lockscreen/impl/src/main/res/values-it/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml
@@ -15,9 +15,9 @@
"Risparmia un po\' di tempo e usa %1$s per sbloccare l\'app ogni volta""Scegli il PIN""Conferma il PIN"
- "Blocca %1$s per aggiungere ulteriore sicurezza alle tue conversazioni.
+ "Blocca %1$s per aggiungere una sicurezza extra alle tue conversazioni.
-Scegli qualcosa facile da ricordare. Se dimentichi questo PIN, verrai disconnesso dall\'app."
+Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’app""Non puoi scegliere questo codice PIN per motivi di sicurezza""Scegli un PIN diverso""Inserisci lo stesso PIN due volte"
diff --git a/features/lockscreen/impl/src/main/res/values-ko/translations.xml b/features/lockscreen/impl/src/main/res/values-ko/translations.xml
new file mode 100644
index 00000000000..b959841a541
--- /dev/null
+++ b/features/lockscreen/impl/src/main/res/values-ko/translations.xml
@@ -0,0 +1,36 @@
+
+
+ "생체 인식 인증"
+ "생체 인식 잠금 해제"
+ "생체 인증으로 잠금 해제"
+ "생체 인식 확인"
+ "PIN을 잊으셨나요?"
+ "PIN 코드 변경"
+ "생체 인식 잠금 해제 허용"
+ "PIN 제거"
+ "PIN을 제거하시겠습니까?"
+ "PIN을 제거하시겠습니까?"
+ "%1$s 허용"
+ "나는 PIN을 사용하고 싶습니다"
+ "시간을 절약하려면 %1$s 를 사용하여 앱을 매번 잠금 해제하세요."
+ "PIN을 선택하세요"
+ "PIN 확인"
+ "%1$s 를 잠그면 채팅에 추가 보안이 적용됩니다.
+
+기억하기 쉬운 것을 선택하세요. 이 PIN을 잊어버리면 앱에서 로그아웃됩니다."
+ "보안상의 이유로 이 코드를 PIN 코드로 선택할 수 없습니다."
+ "다른 PIN을 선택하세요"
+ "PIN을 두 번 입력하세요."
+ "PIN이 일치하지 않습니다"
+ "계속하려면 다시 로그인하고 새로운 PIN을 생성해야 합니다"
+ "로그아웃 중입니다"
+
+ "당신은 %1$d 회 잠금 해제 시도를 가지고 있습니다"
+
+
+ "PIN이 잘못되었습니다. %1$d 번 남았습니다"
+
+ "생체 인증 사용"
+ "PIN 사용"
+ "로그아웃 중…"
+
diff --git a/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml b/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml
index ab3ffc4c06d..f2f67dca6da 100644
--- a/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml
@@ -1,7 +1,7 @@
- "autenticação por biometria"
- "desbloqueio por biometria"
+ "autenticação biométrica"
+ "desbloqueio biométrico""Desbloquear com biometria""Confirmar biometria""Esqueceu o PIN?"
@@ -20,13 +20,13 @@
Escolha algo memorável. Se você esquecer este PIN, você será desconectado do app.""Você não pode escolher este PIN por razões de segurança""Escolha um PIN diferente"
- "Por favor, insira o mesmo PIN duas vezes"
+ "Por favor, digite o mesmo PIN duas vezes""Os PINs não correspondem"
- "Você terá que fazer login novamente e criar um novo PIN para prosseguir"
+ "Você terá que entrar novamente e criar um PIN novo para continuar""Você está sendo desconectado"
- "Você tem %1$d tentativa de debloqueio"
- "Você tem %1$d tentativas de debloqueio"
+ "Você tem %1$d tentativa de desbloqueio"
+ "Você tem %1$d tentativas de desbloqueio""PIN incorreto. Você tem mais %1$d chance"
diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml
index 69ecc93689a..d40bfbaecec 100644
--- a/features/lockscreen/impl/src/main/res/values-ro/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml
@@ -3,12 +3,13 @@
"autentificare biometrică""deblocare biometrică""Deblocați cu biometrice"
+ "Confirmați datele biometrice""Ați uitat codul PIN?""Schimbați codul PIN""Permite deblocarea biometrică"
- "Eliminați codul PIN"
- "Sunteți sigur că doriți să eliminați codul PIN?"
- "Eliminați codul PIN?"
+ "Ștergeți codul PIN"
+ "Sunteți sigur că doriți să ștergeți codul PIN?"
+ "Ștergeți codul PIN?""Permiteți %1$s""Prefer să folosesc un cod PIN""Economisiți timp și utilizați %1$s pentru a debloca aplicația de fiecare dată."
diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml
index eae4799f937..3879c46597a 100644
--- a/features/lockscreen/impl/src/main/res/values-ru/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml
@@ -6,7 +6,7 @@
"Подтвердить биометрические данные""Забыли PIN-код?""Изменить PIN-код"
- "Разрешить биометрическую разблокировку"
+ "Разрешить разблокировку по биометрии""Удалить PIN-код""Вы действительно хотите удалить PIN-код?""Удалить PIN-код?"
diff --git a/features/lockscreen/impl/src/main/res/values-uz/translations.xml b/features/lockscreen/impl/src/main/res/values-uz/translations.xml
index b9f1c4f5cb5..9981cf3bceb 100644
--- a/features/lockscreen/impl/src/main/res/values-uz/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-uz/translations.xml
@@ -1,4 +1,37 @@
+ "biometrik autentifikatsiya"
+ "biometrik qulf ochish"
+ "Biometrik bilan qulfni oching"
+ "PIN kodni unutdingizmi?"
+ "PIN kodni o\'zgartirish"
+ "Biometrik qulfni ochishga ruxsat bering"
+ "PIN-kodni olib tashlang"
+ "Haqiqatan ham PIN kodni olib tashlamoqchimisiz?"
+ "PIN kod olib tashlansinmi?"
+ "Ruxsat berish %1$s"
+ "Men PIN kod ishlatishni maʼqul koʻraman"
+ "Oʻzingizga vaqt tejang va har safar ilovani ochish uchun %1$s dan foydalaning"
+ "PIN kodni tanlang"
+ "PIN kodni tasdiqlang"
+ "Qulflash %1$s suhbatlaringizga qoʻshimcha xavfsizlik qoʻshish uchun.
+
+Esda qoladigan biror narsani tanlang. Agar ushbu PIN kodni unutib qolsangiz, dasturdan chiqib ketasiz."
+ "Xavfsizlik sabablari bilan buni PIN kodingiz sifatida tanlay olmaysiz"
+ "Boshqa PIN kod tanlang"
+ "Iltimos, bir xil PIN kodni ikkita marta kiriting"
+ "PIN kodlar bir-biriga mos kelmadi"
+ "Davom etish uchun qayta kirishingiz va yangi PIN yaratishingiz kerak boʻladi."
+ "Siz tizimdan chiqmoqdasiz"
+
+ "Sizda %1$d ta ochishga urinish mavjud"
+ "Sizda %1$d ta ochishga urinish mavjud"
+
+
+ "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"
+ "Notoʻgʻri PIN. Sizda yana %1$d ta imkoniyat bor"
+
+ "Biometrikdan foydalaning"
+ "PIN koddan foydalaning""Chiqish…"
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt
new file mode 100644
index 00000000000..e99223735f0
--- /dev/null
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.lockscreen.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DefaultLockScreenEntryPointIntentTest {
+ @Test
+ fun `test pin unlock intent`() {
+ val entryPoint = DefaultLockScreenEntryPoint()
+ val result = entryPoint.pinUnlockIntent(InstrumentationRegistry.getInstrumentation().context)
+ assertThat(result.component?.className).isEqualTo(PinUnlockActivity::class.qualifiedName)
+ }
+}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.kt
new file mode 100644
index 00000000000..533a7be4147
--- /dev/null
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.lockscreen.impl
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.api.LockScreenEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.node.TestParentNode
+import org.junit.Rule
+import org.junit.Test
+
+class DefaultLockScreenEntryPointTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ @Test
+ fun `test node builder Setup`() {
+ val entryPoint = DefaultLockScreenEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ LockScreenFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ )
+ }
+ val callback = object : LockScreenEntryPoint.Callback {
+ override fun onSetupDone() = lambdaError()
+ }
+ val navTarget = LockScreenEntryPoint.Target.Setup
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ navTarget = navTarget,
+ callback = callback,
+ )
+ assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
+ assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup))
+ assertThat(result.plugins).contains(callback)
+ }
+
+ @Test
+ fun `test node builder Settings`() {
+ val entryPoint = DefaultLockScreenEntryPoint()
+ val parentNode = TestParentNode.create { buildContext, plugins ->
+ LockScreenFlowNode(
+ buildContext = buildContext,
+ plugins = plugins,
+ )
+ }
+ val callback = object : LockScreenEntryPoint.Callback {
+ override fun onSetupDone() = lambdaError()
+ }
+ val navTarget = LockScreenEntryPoint.Target.Settings
+ val result = entryPoint.createNode(
+ parentNode = parentNode,
+ buildContext = BuildContext.root(null),
+ navTarget = navTarget,
+ callback = callback,
+ )
+ assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
+ assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings))
+ assertThat(result.plugins).contains(callback)
+ }
+}
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt
new file mode 100644
index 00000000000..9082f20a553
--- /dev/null
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.lockscreen.impl
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
+import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
+import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
+import io.element.android.features.lockscreen.impl.pin.PinCodeManager
+import io.element.android.features.lockscreen.impl.pin.createDefaultPinCodeManager
+import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
+import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
+import io.element.android.services.appnavstate.api.AppForegroundStateService
+import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultLockScreenServiceTest {
+ @Test
+ fun `when the pin is not mandatory and no pin is configured isSetupRequired emits false`() = runTest {
+ val sut = createDefaultLockScreenService(
+ lockScreenConfig = aLockScreenConfig(isPinMandatory = false)
+ )
+ sut.isSetupRequired().test {
+ assertThat(awaitItem()).isFalse()
+ }
+ }
+
+ @Test
+ fun `when the pin is mandatory, isSetupRequired emits true`() = runTest {
+ val lockScreenStore = InMemoryLockScreenStore()
+ val sut = createDefaultLockScreenService(
+ lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
+ lockScreenStore = lockScreenStore,
+ )
+ sut.isSetupRequired().test {
+ assertThat(awaitItem()).isTrue()
+ // When the user configures the pin code, the setup is not required anymore
+ lockScreenStore.saveEncryptedPinCode("encryptedCode")
+ assertThat(awaitItem()).isFalse()
+ // Users deletes the pin code
+ lockScreenStore.deleteEncryptedPinCode()
+ assertThat(awaitItem()).isTrue()
+ }
+ }
+
+ @Test
+ fun `when the last session is deleted, the pin code is removed`() = runTest {
+ val sessionObserver = FakeSessionObserver()
+ val lockScreenStore = InMemoryLockScreenStore()
+ val sut = createDefaultLockScreenService(
+ lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
+ lockScreenStore = lockScreenStore,
+ sessionObserver = sessionObserver,
+ )
+ sut.isPinSetup().test {
+ assertThat(awaitItem()).isFalse()
+ // When the user configure the pin code, the setup is not required anymore
+ lockScreenStore.saveEncryptedPinCode("encryptedCode")
+ assertThat(awaitItem()).isTrue()
+ sessionObserver.onSessionDeleted("userId", wasLastSession = false)
+ expectNoEvents()
+ sessionObserver.onSessionDeleted("userId", wasLastSession = true)
+ assertThat(awaitItem()).isFalse()
+ }
+ }
+}
+
+private fun TestScope.createDefaultLockScreenService(
+ lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
+ lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
+ pinCodeManager: PinCodeManager = createDefaultPinCodeManager(
+ lockScreenStore = lockScreenStore,
+ ),
+ sessionObserver: SessionObserver = FakeSessionObserver(),
+ appForegroundStateService: AppForegroundStateService = FakeAppForegroundStateService(),
+ biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
+) = DefaultLockScreenService(
+ lockScreenConfig = lockScreenConfig,
+ lockScreenStore = lockScreenStore,
+ pinCodeManager = pinCodeManager,
+ coroutineScope = backgroundScope,
+ sessionObserver = sessionObserver,
+ appForegroundStateService = appForegroundStateService,
+ biometricAuthenticatorManager = biometricAuthenticatorManager,
+)
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt
index 5e35f22e053..073bdc799dd 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt
index 6e98abd80cf..9e9b892582a 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
index 675102140d6..23fccb6584b 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
index 5f983daaceb..89bd760a5f4 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
index ed942c45b99..dbc9c18c90a 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,19 +11,18 @@ package io.element.android.features.lockscreen.impl.pin
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
+import io.element.android.features.lockscreen.impl.storage.LockScreenStore
+import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
+import io.element.android.libraries.cryptography.api.SecretKeyRepository
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPinCodeManagerTest {
- private val lockScreenStore = InMemoryLockScreenStore()
- private val secretKeyRepository = SimpleSecretKeyRepository()
- private val encryptionDecryptionService = AESEncryptionDecryptionService()
- private val pinCodeManager = DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
-
@Test
fun `given a pin code when create and delete assert no pin code left`() = runTest {
+ val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.hasPinCode().test {
assertThat(awaitItem()).isFalse()
pinCodeManager.createPinCode("1234")
@@ -34,6 +34,7 @@ class DefaultPinCodeManagerTest {
@Test
fun `given a pin code when create and verify with the same pin succeed`() = runTest {
+ val pinCodeManager = createDefaultPinCodeManager()
val pinCode = "1234"
pinCodeManager.createPinCode(pinCode)
assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue()
@@ -41,7 +42,18 @@ class DefaultPinCodeManagerTest {
@Test
fun `given a pin code when create and verify with a different pin fails`() = runTest {
+ val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.createPinCode("1234")
assertThat(pinCodeManager.verifyPinCode("1235")).isFalse()
}
}
+
+fun createDefaultPinCodeManager(
+ lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
+ secretKeyRepository: SecretKeyRepository = SimpleSecretKeyRepository(),
+ encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
+) = DefaultPinCodeManager(
+ lockScreenStore = lockScreenStore,
+ secretKeyRepository = secretKeyRepository,
+ encryptionDecryptionService = encryptionDecryptionService,
+)
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt
index b704e24e0d1..9a863a04780 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt
index 2c59f843bcf..8c2e5ba4bc8 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
index a4a197b533b..61acf71cdda 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
index 45af2682e12..ef3e94f27f1 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt
index 7efb6619f15..3f87c1dccf2 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
index 1d1b381fef3..6a1d32e8793 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
index 5fdf1342eae..f5bfb118189 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt
index 389ebf83c73..34244b3374c 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt
index e51b007312c..1ecb79bd67e 100644
--- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt
+++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/lockscreen/test/build.gradle.kts b/features/lockscreen/test/build.gradle.kts
index 80f46e82c62..5c314dbb872 100644
--- a/features/lockscreen/test/build.gradle.kts
+++ b/features/lockscreen/test/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -14,6 +15,8 @@ android {
}
dependencies {
- implementation(libs.coroutines.core)
api(projects.features.lockscreen.api)
+ implementation(libs.coroutines.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.tests.testutils)
}
diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenEntryPoint.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenEntryPoint.kt
new file mode 100644
index 00000000000..1bfcbaf45b5
--- /dev/null
+++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenEntryPoint.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.lockscreen.test
+
+import android.content.Context
+import android.content.Intent
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import io.element.android.features.lockscreen.api.LockScreenEntryPoint
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeLockScreenEntryPoint : LockScreenEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ navTarget: LockScreenEntryPoint.Target,
+ callback: LockScreenEntryPoint.Callback,
+ ): Node = lambdaError()
+
+ override fun pinUnlockIntent(context: Context): Intent = lambdaError()
+}
diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt
index 7d2fd60c16e..a75b3b1614c 100644
--- a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt
+++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/api/build.gradle.kts b/features/login/api/build.gradle.kts
index 869e7ad25a9..2a4e3ecb7c1 100644
--- a/features/login/api/build.gradle.kts
+++ b/features/login/api/build.gradle.kts
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
index b3ef99ee198..5256bcb695f 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -19,14 +20,13 @@ interface LoginEntryPoint : FeatureEntryPoint {
)
interface Callback : Plugin {
- fun onReportProblem()
+ fun navigateToBugReport()
}
- fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
-
- interface NodeBuilder {
- fun params(params: Params): NodeBuilder
- fun callback(callback: Callback): NodeBuilder
- fun build(): Node
- }
+ fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: Params,
+ callback: Callback,
+ ): Node
}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt
index 67eb284944d..79cfc35fbd2 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt
index 84dcac40a65..2a83a069150 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt
deleted file mode 100644
index 9b0cce7b989..00000000000
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.login.api
-
-import kotlinx.coroutines.flow.StateFlow
-
-interface LoginUserStory {
- val loginFlowIsDone: StateFlow
-}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt
new file mode 100644
index 00000000000..fc804c14ffe
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.api.accesscontrol
+
+interface AccountProviderAccessControl {
+ suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String): Boolean
+}
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index a4f20fb97df..071408b2f54 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -1,10 +1,11 @@
-import extension.ComponentMergingStrategy
-import extension.setupAnvil
+import extension.setupDependencyInjection
+import extension.testCommonDependencies
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -24,7 +25,7 @@ android {
}
}
-setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
+setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
@@ -36,35 +37,27 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.api)
- implementation(projects.libraries.network)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.permissions.api)
+ implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api)
implementation(projects.libraries.uiUtils)
+ implementation(projects.libraries.wellknown.api)
implementation(libs.androidx.browser)
- implementation(platform(libs.network.retrofit.bom))
implementation(libs.androidx.webkit)
- implementation(libs.network.retrofit)
implementation(libs.serialization.json)
api(projects.features.login.api)
- testImplementation(libs.test.junit)
- testImplementation(libs.androidx.compose.ui.test.junit)
- testImplementation(libs.androidx.test.ext.junit)
- testImplementation(libs.coroutines.test)
- testImplementation(libs.molecule.runtime)
- testImplementation(libs.test.robolectric)
- testImplementation(libs.test.truth)
- testImplementation(libs.test.turbine)
+ testCommonDependencies(libs, true)
testImplementation(projects.features.login.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.permissions.test)
- testImplementation(projects.tests.testutils)
- testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+ testImplementation(projects.libraries.sessionStorage.test)
+ testImplementation(projects.libraries.wellknown.test)
}
diff --git a/features/login/impl/src/main/AndroidManifest.xml b/features/login/impl/src/main/AndroidManifest.xml
index 44281caee1b..453cf051320 100644
--- a/features/login/impl/src/main/AndroidManifest.xml
+++ b/features/login/impl/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
index b6eca77a497..1f0fe44f358 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -9,35 +10,28 @@ package io.element.android.features.login.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.libraries.architecture.createNode
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
- override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LoginEntryPoint.NodeBuilder {
- val plugins = ArrayList()
-
- return object : LoginEntryPoint.NodeBuilder {
- override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder {
- plugins += LoginFlowNode.Params(
+class DefaultLoginEntryPoint : LoginEntryPoint {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ params: LoginEntryPoint.Params,
+ callback: LoginEntryPoint.Callback,
+ ): Node {
+ return parentNode.createNode(
+ buildContext = buildContext,
+ plugins = listOf(
+ LoginFlowNode.Params(
accountProvider = params.accountProvider,
loginHint = params.loginHint,
- )
- return this
- }
-
- override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
- plugins += callback
- return this
- }
-
- override fun build(): Node {
- return parentNode.createNode(buildContext, plugins)
- }
- }
+ ),
+ callback,
+ )
+ )
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt
index 4b17980fa6d..26f0f1737e9 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt
@@ -1,21 +1,21 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl
import androidx.core.net.toUri
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
@ContributesBinding(AppScope::class)
-class DefaultLoginIntentResolver @Inject constructor() : LoginIntentResolver {
+class DefaultLoginIntentResolver : LoginIntentResolver {
override fun parse(uriString: String): LoginParams? {
val uri = uriString.toUri()
if (uri.host != "mobile.element.io") return null
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt
deleted file mode 100644
index eac64b39e37..00000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.login.impl
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.login.api.LoginUserStory
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
-import kotlinx.coroutines.flow.MutableStateFlow
-import javax.inject.Inject
-
-@SingleIn(AppScope::class)
-@ContributesBinding(AppScope::class)
-class DefaultLoginUserStory @Inject constructor() : LoginUserStory {
- // True by default, will be set to false when the login user story is started, and set to true again once it's done.
- override val loginFlowIsDone: MutableStateFlow = MutableStateFlow(true)
-
- fun setLoginFlowIsDone(value: Boolean) {
- loginFlowIsDone.value = value
- }
-}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index 2d791f0f9a1..4a7275e2dad 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -18,13 +19,13 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
-import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
@@ -40,23 +41,27 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
-import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
-class LoginFlowNode @AssistedInject constructor(
+@AssistedInject
+class LoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
private val accountProviderDataSource: AccountProviderDataSource,
- private val defaultLoginUserStory: DefaultLoginUserStory,
private val oidcActionFlow: OidcActionFlow,
+ @AppCoroutineScope
+ private val appCoroutineScope: CoroutineScope,
) : BaseFlowNode(
backstack = BackStack(
initialElement = NavTarget.OnBoarding,
@@ -70,6 +75,7 @@ class LoginFlowNode @AssistedInject constructor(
val loginHint: String?,
) : NodeInputs
+ private val callback: LoginEntryPoint.Callback = callback()
private var activity: Activity? = null
private var darkTheme: Boolean = false
@@ -77,7 +83,6 @@ class LoginFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
- defaultLoginUserStory.setLoginFlowIsDone(false)
lifecycle.subscribe(
onResume = {
if (externalAppStarted) {
@@ -88,7 +93,7 @@ class LoginFlowNode @AssistedInject constructor(
// by pressing back or by closing the Custom Chrome Tab.
lifecycleScope.launch {
delay(5000)
- oidcActionFlow.post(OidcAction.GoBack)
+ oidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
}
}
}
@@ -127,13 +132,13 @@ class LoginFlowNode @AssistedInject constructor(
return when (navTarget) {
NavTarget.OnBoarding -> {
val callback = object : OnBoardingNode.Callback {
- override fun onSignUp() {
+ override fun navigateToSignUpFlow() {
backstack.push(
NavTarget.ConfirmAccountProvider(isAccountCreation = true)
)
}
- override fun onSignIn(mustChooseAccountProvider: Boolean) {
+ override fun navigateToSignInFlow(mustChooseAccountProvider: Boolean) {
backstack.push(
if (mustChooseAccountProvider) {
NavTarget.ChooseAccountProvider
@@ -143,23 +148,23 @@ class LoginFlowNode @AssistedInject constructor(
)
}
- override fun onSignInWithQrCode() {
+ override fun navigateToQrCode() {
backstack.push(NavTarget.QrCode)
}
- override fun onReportProblem() {
- plugins().forEach { it.onReportProblem() }
+ override fun navigateToBugReport() {
+ callback.navigateToBugReport()
}
- override fun onOidcDetails(oidcDetails: OidcDetails) {
+ override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails)
}
- override fun onCreateAccountContinue(url: String) {
+ override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url))
}
- override fun onLoginPasswordNeeded() {
+ override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword)
}
}
@@ -172,15 +177,15 @@ class LoginFlowNode @AssistedInject constructor(
}
NavTarget.ChooseAccountProvider -> {
val callback = object : ChooseAccountProviderNode.Callback {
- override fun onOidcDetails(oidcDetails: OidcDetails) {
+ override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails)
}
- override fun onCreateAccountContinue(url: String) {
+ override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url))
}
- override fun onLoginPasswordNeeded() {
+ override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword)
}
}
@@ -194,19 +199,19 @@ class LoginFlowNode @AssistedInject constructor(
isAccountCreation = navTarget.isAccountCreation,
)
val callback = object : ConfirmAccountProviderNode.Callback {
- override fun onOidcDetails(oidcDetails: OidcDetails) {
+ override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails)
}
- override fun onCreateAccountContinue(url: String) {
+ override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url))
}
- override fun onLoginPasswordNeeded() {
+ override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword)
}
- override fun onChangeAccountProvider() {
+ override fun navigateToChangeAccountProvider() {
backstack.push(NavTarget.ChangeAccountProvider)
}
}
@@ -222,7 +227,7 @@ class LoginFlowNode @AssistedInject constructor(
backstack.singleTop(confirmAccountProvider)
}
- override fun onOtherClick() {
+ override fun navigateToSearchAccountProvider() {
backstack.push(NavTarget.SearchAccountProvider)
}
}
@@ -268,7 +273,9 @@ class LoginFlowNode @AssistedInject constructor(
DisposableEffect(Unit) {
onDispose {
activity = null
- accountProviderDataSource.reset()
+ appCoroutineScope.launch {
+ accountProviderDataSource.reset()
+ }
}
}
BackstackView()
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt
new file mode 100644
index 00000000000..db56047e15c
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.accesscontrol
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesBinding
+import io.element.android.features.enterprise.api.EnterpriseService
+import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
+import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
+import io.element.android.libraries.core.uri.ensureProtocol
+import io.element.android.libraries.wellknown.api.WellknownRetriever
+
+@ContributesBinding(AppScope::class)
+class DefaultAccountProviderAccessControl(
+ private val enterpriseService: EnterpriseService,
+ private val wellknownRetriever: WellknownRetriever,
+) : AccountProviderAccessControl {
+ override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
+ assertIsAllowedToConnectToAccountProvider(
+ title = accountProviderUrl,
+ accountProviderUrl = accountProviderUrl,
+ )
+ true
+ } catch (_: AccountProviderAccessException) {
+ false
+ }
+
+ @Throws(AccountProviderAccessException::class)
+ suspend fun assertIsAllowedToConnectToAccountProvider(
+ title: String,
+ accountProviderUrl: String,
+ ) {
+ if (enterpriseService.isEnterpriseBuild.not()) {
+ // Ensure that Element Pro is not required for this account provider
+ val wellKnown = wellknownRetriever.getElementWellKnown(
+ baseUrl = accountProviderUrl.ensureProtocol(),
+ ).dataOrNull()
+ if (wellKnown?.enforceElementPro == true) {
+ throw AccountProviderAccessException.NeedElementProException(
+ unauthorisedAccountProviderTitle = title,
+ applicationId = ELEMENT_PRO_APPLICATION_ID,
+ )
+ }
+ }
+ if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
+ throw AccountProviderAccessException.UnauthorizedAccountProviderException(
+ unauthorisedAccountProviderTitle = title,
+ authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
+ )
+ }
+ }
+
+ companion object {
+ const val ELEMENT_PRO_APPLICATION_ID = "io.element.enterprise"
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt
index 0fcef6bedf6..7ca467da7d3 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,5 +14,4 @@ data class AccountProvider(
val subtitle: String? = null,
val isPublic: Boolean = false,
val isMatrixOrg: Boolean = false,
- val isValid: Boolean = false,
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt
index 9ebc246e25b..931959d4eac 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt
@@ -1,47 +1,55 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.accountprovider
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Inject
+import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import javax.inject.Inject
@SingleIn(AppScope::class)
-class AccountProviderDataSource @Inject constructor(
+@Inject
+class AccountProviderDataSource(
enterpriseService: EnterpriseService,
) {
- private val defaultAccountProvider =
- (enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
- .let { url ->
- AccountProvider(
- url = url,
- subtitle = null,
- isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
- isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
- )
- }
-
- private val accountProvider: MutableStateFlow = MutableStateFlow(
- defaultAccountProvider
+ private val defaultAccountProvider = createAccountProvider(
+ url = enterpriseService.defaultHomeserverList()
+ .firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
+ ?: AuthenticationConfig.MATRIX_ORG_URL
)
+ private val accountProvider: MutableStateFlow = MutableStateFlow(defaultAccountProvider)
+
val flow: StateFlow = accountProvider.asStateFlow()
- fun reset() {
- accountProvider.tryEmit(defaultAccountProvider)
+ suspend fun reset() {
+ accountProvider.emit(defaultAccountProvider)
+ }
+
+ suspend fun setUrl(url: String) {
+ setAccountProvider(createAccountProvider(url))
+ }
+
+ suspend fun setAccountProvider(data: AccountProvider) {
+ accountProvider.emit(data)
}
- fun userSelection(data: AccountProvider) {
- accountProvider.tryEmit(data)
+ private fun createAccountProvider(url: String): AccountProvider {
+ return AccountProvider(
+ url = url,
+ subtitle = null,
+ isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
+ isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
+ )
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt
index 92fda4cf14a..fd5c7b8019a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt
index a5f0fd7d3ba..745939233f6 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -15,7 +16,7 @@ open class AccountProviderProvider : PreviewParameterProvider {
get() = sequenceOf(
anAccountProvider(),
anAccountProvider().copy(subtitle = null),
- anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false),
+ anAccountProvider().copy(subtitle = null, title = "invalid"),
anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false),
// Add other state here
)
@@ -26,11 +27,9 @@ fun anAccountProvider(
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
isPublic: Boolean = true,
isMatrixOrg: Boolean = true,
- isValid: Boolean = true,
) = AccountProvider(
url = url,
subtitle = subtitle,
isPublic = isPublic,
isMatrixOrg = isMatrixOrg,
- isValid = isValid,
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
index b61f0283a71..5130bf1ad89 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/AccountProviderAccessException.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/AccountProviderAccessException.kt
new file mode 100644
index 00000000000..88ec3bfe8e8
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/AccountProviderAccessException.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.changeserver
+
+sealed class AccountProviderAccessException : Exception() {
+ data class NeedElementProException(
+ val unauthorisedAccountProviderTitle: String,
+ val applicationId: String,
+ ) : AccountProviderAccessException()
+
+ data class UnauthorizedAccountProviderException(
+ val unauthorisedAccountProviderTitle: String,
+ val authorisedAccountProviderTitles: List,
+ ) : AccountProviderAccessException()
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt
index 231fe04ba0d..27e6128dd07 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt
index c0cdf7c06d2..4a4fb3ca412 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,7 +13,8 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import io.element.android.features.enterprise.api.EnterpriseService
+import dev.zacsweers.metro.Inject
+import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
@@ -22,12 +24,12 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class ChangeServerPresenter @Inject constructor(
+@Inject
+class ChangeServerPresenter(
private val authenticationService: MatrixAuthenticationService,
private val accountProviderDataSource: AccountProviderDataSource,
- private val enterpriseService: EnterpriseService,
+ private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
) : Presenter {
@Composable
override fun present(): ChangeServerState {
@@ -37,7 +39,7 @@ class ChangeServerPresenter @Inject constructor(
mutableStateOf(AsyncData.Uninitialized)
}
- fun handleEvents(event: ChangeServerEvents) {
+ fun handleEvent(event: ChangeServerEvents) {
when (event) {
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
@@ -46,7 +48,7 @@ class ChangeServerPresenter @Inject constructor(
return ChangeServerState(
changeServerAction = changeServerAction.value,
- eventSink = ::handleEvents
+ eventSink = ::handleEvent,
)
}
@@ -55,17 +57,16 @@ class ChangeServerPresenter @Inject constructor(
changeServerAction: MutableState>,
) = launch {
suspend {
- if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) {
- throw UnauthorizedAccountProviderException(
- unauthorisedAccountProviderTitle = data.title,
- authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
- )
+ defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
+ title = data.title,
+ accountProviderUrl = data.url,
+ )
+ val details = authenticationService.setHomeserver(data.url).getOrThrow()
+ if (!details.isSupported) {
+ throw ChangeServerError.UnsupportedServer
}
- authenticationService.setHomeserver(data.url).map {
- authenticationService.getHomeserverDetails().value!!
- // Valid, remember user choice
- accountProviderDataSource.userSelection(data)
- }.getOrThrow()
+ // Homeserver is valid, remember user choice
+ accountProviderDataSource.setAccountProvider(data)
}.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from)
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt
index fb5b79474e1..eea3ad78cd4 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt
index 2549109488c..1a94bb56315 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,13 +11,12 @@ package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.AsyncData
-import io.element.android.libraries.ui.strings.CommonStrings
open class ChangeServerStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aChangeServerState(),
- aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))),
+ aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(null))),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
aChangeServerState(
changeServerAction = AsyncData.Failure(
@@ -26,6 +26,19 @@ open class ChangeServerStateProvider : PreviewParameterProvider Unit,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
val eventSink = state.eventSink
when (state.changeServerAction) {
is AsyncData.Failure -> {
when (val error = state.changeServerAction.error as? ChangeServerError) {
+ ChangeServerError.InvalidServer ->
+ ErrorDialog(
+ modifier = modifier,
+ content = stringResource(R.string.screen_change_server_error_invalid_homeserver),
+ onSubmit = {
+ eventSink.invoke(ChangeServerEvents.ClearError)
+ }
+ )
+ ChangeServerError.UnsupportedServer ->
+ ErrorDialog(
+ modifier = modifier,
+ content = stringResource(R.string.screen_login_error_unsupported_authentication),
+ onSubmit = {
+ eventSink.invoke(ChangeServerEvents.ClearError)
+ }
+ )
is ChangeServerError.Error -> {
ErrorDialog(
modifier = modifier,
- content = error.message(),
+ content = error.messageStr ?: stringResource(CommonStrings.error_unknown),
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
@@ -56,6 +78,24 @@ fun ChangeServerView(
}
)
}
+ is ChangeServerError.NeedElementPro -> {
+ ConfirmationDialog(
+ modifier = modifier,
+ title = stringResource(R.string.screen_change_server_error_element_pro_required_title),
+ content = stringResource(
+ R.string.screen_change_server_error_element_pro_required_message,
+ error.unauthorisedAccountProviderTitle,
+ ),
+ submitText = stringResource(R.string.screen_change_server_error_element_pro_required_action_android),
+ onSubmitClick = {
+ context.openGooglePlay(error.applicationId)
+ eventSink.invoke(ChangeServerEvents.ClearError)
+ },
+ onDismiss = {
+ eventSink.invoke(ChangeServerEvents.ClearError)
+ },
+ )
+ }
is ChangeServerError.UnauthorizedAccountProvider -> {
ErrorDialog(
modifier = modifier,
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt
deleted file mode 100644
index 1ceb2ab3437..00000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright 2025 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.login.impl.changeserver
-
-class UnauthorizedAccountProviderException(
- val unauthorisedAccountProviderTitle: String,
- val authorisedAccountProviderTitles: List,
-) : Exception()
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt
index 13835ea65ca..4523e6f45e8 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt
@@ -1,22 +1,23 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.di
-import com.squareup.anvil.annotations.ContributesTo
-import dagger.Binds
-import dagger.Module
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.BindingContainer
+import dev.zacsweers.metro.Binds
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
-@Module
+@BindingContainer
interface LoginModule {
@Binds
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt
index cc328d6e86e..050dc0c8c65 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt
@@ -1,13 +1,14 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.di
-import com.squareup.anvil.annotations.ContributesTo
+import dev.zacsweers.metro.ContributesTo
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
@ContributesTo(QrCodeLoginScope::class)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt
deleted file mode 100644
index 7f1ffc02853..00000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.login.impl.di
-
-import com.squareup.anvil.annotations.ContributesTo
-import com.squareup.anvil.annotations.MergeSubcomponent
-import io.element.android.libraries.architecture.NodeFactoriesBindings
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.SingleIn
-
-@SingleIn(QrCodeLoginScope::class)
-@MergeSubcomponent(QrCodeLoginScope::class)
-interface QrCodeLoginComponent : NodeFactoriesBindings {
- @MergeSubcomponent.Builder
- interface Builder {
- fun build(): QrCodeLoginComponent
- }
-
- @ContributesTo(AppScope::class)
- interface ParentBindings {
- fun qrCodeLoginComponentBuilder(): Builder
- }
-}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.kt
new file mode 100644
index 00000000000..65008206293
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginGraph.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.di
+
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.ContributesTo
+import dev.zacsweers.metro.GraphExtension
+import io.element.android.libraries.architecture.NodeFactoriesBindings
+
+@GraphExtension(QrCodeLoginScope::class)
+interface QrCodeLoginGraph : NodeFactoriesBindings {
+ @ContributesTo(AppScope::class)
+ @GraphExtension.Factory
+ interface Factory {
+ fun create(): QrCodeLoginGraph
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt
index c4aaf4f1b3f..a3538c8cd39 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
index be526ccb0d1..60ffd2af87e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt
index 6d678854c1f..2f4af142372 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt
@@ -1,30 +1,25 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.error
-import androidx.annotation.StringRes
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.ui.res.stringResource
-import io.element.android.features.login.impl.R
-import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
+import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.libraries.matrix.api.auth.AuthenticationException
-import io.element.android.libraries.ui.strings.CommonStrings
-sealed class ChangeServerError : Throwable() {
+sealed class ChangeServerError : Exception() {
data class Error(
- @StringRes val messageId: Int? = null,
val messageStr: String? = null,
- ) : ChangeServerError() {
- @Composable
- @ReadOnlyComposable
- fun message(): String = messageStr ?: stringResource(messageId ?: CommonStrings.error_unknown)
- }
+ ) : ChangeServerError()
+
+ data class NeedElementPro(
+ val unauthorisedAccountProviderTitle: String,
+ val applicationId: String,
+ ) : ChangeServerError()
data class UnauthorizedAccountProvider(
val unauthorisedAccountProviderTitle: String,
@@ -32,16 +27,32 @@ sealed class ChangeServerError : Throwable() {
) : ChangeServerError()
data object SlidingSyncAlert : ChangeServerError()
+ data object InvalidServer : ChangeServerError()
+ data object UnsupportedServer : ChangeServerError()
companion object {
fun from(error: Throwable): ChangeServerError = when (error) {
- is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
- is AuthenticationException.Oidc -> Error(messageStr = error.message)
- is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(
+ is ChangeServerError -> error
+ is AuthenticationException -> {
+ when (error) {
+ is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
+ is AuthenticationException.InvalidServerName,
+ is AuthenticationException.ServerUnreachable -> InvalidServer
+ // AccountAlreadyLoggedIn error should not happen at this point
+ is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message)
+ is AuthenticationException.Generic -> Error(messageStr = error.message)
+ is AuthenticationException.Oidc -> Error(messageStr = error.message)
+ }
+ }
+ is AccountProviderAccessException.NeedElementProException -> NeedElementPro(
+ unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
+ applicationId = error.applicationId,
+ )
+ is AccountProviderAccessException.UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
authorisedAccountProviderTitles = error.authorisedAccountProviderTitles,
)
- else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
+ else -> Error(messageStr = error.message)
}
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt
new file mode 100644
index 00000000000..59fb42fe0ca
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.error
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+class ChangeServerErrorProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ ChangeServerError.InvalidServer,
+ ChangeServerError.Error(
+ messageStr = "An error description",
+ ),
+ ChangeServerError.NeedElementPro(
+ unauthorisedAccountProviderTitle = "element.io",
+ applicationId = "io.element.enterprise",
+ ),
+ ChangeServerError.UnauthorizedAccountProvider(
+ unauthorisedAccountProviderTitle = "element.io",
+ authorisedAccountProviderTitles = listOf("provider.org", "provider.io"),
+ ),
+ ChangeServerError.SlidingSyncAlert,
+ ChangeServerError.UnsupportedServer,
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt
index 16bdf9b5e67..66ca0d97521 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2022-2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2022-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt
index 459f45a06c7..a62919e7054 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -12,7 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
-import io.element.android.features.login.impl.DefaultLoginUserStory
+import dev.zacsweers.metro.Inject
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
@@ -25,20 +26,17 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import javax.inject.Inject
/**
* This class is responsible for managing the login flow, including handling OIDC actions and
* submitting login requests.
- * It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
+ * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
* and [ChooseAccountProviderPresenter].
*/
-class LoginHelper @Inject constructor(
+@Inject
+class LoginHelper(
private val oidcActionFlow: OidcActionFlow,
private val authenticationService: MatrixAuthenticationService,
- private val defaultLoginUserStory: DefaultLoginUserStory,
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
) {
private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized)
@@ -59,15 +57,13 @@ class LoginHelper @Inject constructor(
loginModeState.value = AsyncData.Uninitialized
}
- fun submit(
- coroutineScope: CoroutineScope,
+ suspend fun submit(
isAccountCreation: Boolean,
homeserverUrl: String,
loginHint: String?,
- ) = coroutineScope.launch {
+ ) {
suspend {
- authenticationService.setHomeserver(homeserverUrl).map {
- val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
+ authenticationService.setHomeserver(homeserverUrl).map { matrixHomeServerDetails ->
if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
@@ -95,9 +91,14 @@ class LoginHelper @Inject constructor(
}
private suspend fun onOidcAction(oidcAction: OidcAction) {
+ if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
+ // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
+ // This can happen if there is an error, for instance attempt to login again on the same account.
+ return
+ }
loginModeState.value = AsyncData.Loading()
when (oidcAction) {
- OidcAction.GoBack -> {
+ is OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginModeState.value = AsyncData.Uninitialized
@@ -108,9 +109,6 @@ class LoginHelper @Inject constructor(
}
is OidcAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url)
- .onSuccess { _ ->
- defaultLoginUserStory.setLoginFlowIsDone(true)
- }
.onFailure { failure ->
loginModeState.value = AsyncData.Failure(failure)
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt
index 571b0182977..08e604ef200 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt
@@ -1,7 +1,8 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
index abf1327913c..f88e34bf4a2 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt
@@ -1,21 +1,29 @@
/*
+ * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.login
import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
+import io.element.android.libraries.androidutils.system.openGooglePlay
import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
+import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.ui.strings.CommonStrings
@@ -28,14 +36,26 @@ fun LoginModeView(
onNeedLoginPassword: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit
) {
+ val context = LocalContext.current
when (loginMode) {
is AsyncData.Failure -> {
when (val error = loginMode.error) {
is ChangeServerError -> {
when (error) {
+ ChangeServerError.InvalidServer ->
+ ErrorDialog(
+ content = stringResource(R.string.screen_change_server_error_invalid_homeserver),
+ onSubmit = onClearError,
+ )
+ is ChangeServerError.UnsupportedServer -> {
+ ErrorDialog(
+ content = stringResource(R.string.screen_login_error_unsupported_authentication),
+ onSubmit = onClearError,
+ )
+ }
is ChangeServerError.Error -> {
ErrorDialog(
- content = error.message(),
+ content = error.messageStr ?: stringResource(CommonStrings.error_unknown),
onSubmit = onClearError,
)
}
@@ -48,6 +68,21 @@ fun LoginModeView(
onDismiss = onClearError,
)
}
+ is ChangeServerError.NeedElementPro -> {
+ ConfirmationDialog(
+ title = stringResource(R.string.screen_change_server_error_element_pro_required_title),
+ content = stringResource(
+ R.string.screen_change_server_error_element_pro_required_message,
+ error.unauthorisedAccountProviderTitle,
+ ),
+ submitText = stringResource(R.string.screen_change_server_error_element_pro_required_action_android),
+ onSubmitClick = {
+ context.openGooglePlay(error.applicationId)
+ onClearError()
+ },
+ onDismiss = onClearError,
+ )
+ }
is ChangeServerError.UnauthorizedAccountProvider -> {
ErrorDialog(
content = stringResource(
@@ -66,6 +101,12 @@ fun LoginModeView(
onSubmit = onClearError,
)
}
+ is AuthenticationException.AccountAlreadyLoggedIn -> {
+ ErrorDialog(
+ content = stringResource(CommonStrings.error_account_already_logged_in, error.userId),
+ onSubmit = onClearError,
+ )
+ }
else -> {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
@@ -87,3 +128,18 @@ fun LoginModeView(
AsyncData.Uninitialized -> Unit
}
}
+
+@PreviewsDayNight
+@Composable
+internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::class) error: Throwable) {
+ ElementPreview {
+ LoginModeView(
+ loginMode = AsyncData.Failure(error),
+ onClearError = {},
+ onLearnMoreClick = {},
+ onOidcDetails = {},
+ onNeedLoginPassword = {},
+ onCreateAccountContinue = {}
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt
new file mode 100644
index 00000000000..513d0a8a040
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.login.impl.login
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.login.impl.error.ChangeServerErrorProvider
+import io.element.android.libraries.matrix.api.auth.AuthenticationException
+
+class LoginModeViewErrorProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = ChangeServerErrorProvider().values +
+ AuthenticationException.AccountAlreadyLoggedIn("@alice:matrix.org")
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt
index 6628a63254a..a25d810c25f 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt
@@ -1,15 +1,16 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.qrcode
-import com.squareup.anvil.annotations.ContributesBinding
+import dev.zacsweers.metro.ContributesBinding
+import dev.zacsweers.metro.SingleIn
import io.element.android.features.login.impl.di.QrCodeLoginScope
-import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@@ -17,11 +18,10 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
@SingleIn(QrCodeLoginScope::class)
@ContributesBinding(QrCodeLoginScope::class)
-class DefaultQrCodeLoginManager @Inject constructor(
+class DefaultQrCodeLoginManager(
private val authenticationService: MatrixAuthenticationService,
) : QrCodeLoginManager {
private val _currentLoginStep = MutableStateFlow(QrCodeLoginStep.Uninitialized)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
index 6956b964641..7dad95741b0 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -21,12 +22,12 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.login.impl.DefaultLoginUserStory
+import dev.zacsweers.metro.AppScope
+import dev.zacsweers.metro.Assisted
+import dev.zacsweers.metro.AssistedInject
+import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginBindings
-import io.element.android.features.login.impl.di.QrCodeLoginComponent
+import io.element.android.features.login.impl.di.QrCodeLoginGraph
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationNode
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
import io.element.android.features.login.impl.screens.qrcode.error.QrCodeErrorNode
@@ -38,8 +39,7 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.DaggerComponentOwner
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
@@ -50,11 +50,11 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class)
-class QrCodeLoginFlowNode @AssistedInject constructor(
+@AssistedInject
+class QrCodeLoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- qrCodeLoginComponentBuilder: QrCodeLoginComponent.Builder,
- private val defaultLoginUserStory: DefaultLoginUserStory,
+ qrCodeLoginGraphFactory: QrCodeLoginGraph.Factory,
private val coroutineDispatchers: CoroutineDispatchers,
) : BaseFlowNode(
backstack = BackStack(
@@ -63,10 +63,10 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
),
buildContext = buildContext,
plugins = plugins,
-), DaggerComponentOwner {
+), DependencyInjectionGraphOwner {
private var authenticationJob: Job? = null
- override val daggerComponent = qrCodeLoginComponentBuilder.build()
+ override val graph = qrCodeLoginGraphFactory.create()
private val qrCodeLoginManager by lazy { bindings().qrCodeLoginManager() }
sealed interface NavTarget : Parcelable {
@@ -148,11 +148,11 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
return when (navTarget) {
is NavTarget.Initial -> {
val callback = object : QrCodeIntroNode.Callback {
- override fun onCancelClicked() {
+ override fun cancel() {
navigateUp()
}
- override fun onContinue() {
+ override fun navigateToQrCodeScan() {
backstack.push(NavTarget.QrCodeScan)
}
}
@@ -160,11 +160,11 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
}
is NavTarget.QrCodeScan -> {
val callback = object : QrCodeScanNode.Callback {
- override fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) {
+ override fun handleScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) {
lifecycleScope.startAuthentication(qrCodeLoginData)
}
- override fun onCancelClicked() {
+ override fun cancel() {
backstack.pop()
}
}
@@ -198,7 +198,6 @@ class QrCodeLoginFlowNode @AssistedInject constructor(
authenticationJob = launch(coroutineDispatchers.main) {
qrCodeLoginManager.authenticate(qrCodeLoginData)
.onSuccess {
- defaultLoginUserStory.setLoginFlowIsDone(true)
authenticationJob = null
}
.onFailure { throwable ->
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt
index f1ae224f563..5f75403443e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2024, 2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
index 0b4b0889389..d9994deb9a6 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
@@ -1,7 +1,8 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,6 +11,4 @@ package io.element.android.features.login.impl.resolver
data class HomeserverData(
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
val homeserverUrl: String,
- // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
- val isWellknownValid: Boolean,
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt
index 6190d6ff6b4..1c4ef8cd09e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt
@@ -1,33 +1,33 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright (c) 2025 Element Creations Ltd.
+ * Copyright 2023-2025 New Vector Ltd.
*
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.resolver
-import io.element.android.features.login.impl.resolver.network.WellknownRequest
-import io.element.android.libraries.core.bool.orFalse
+import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.parallelMap
-import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl
+import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeout
+import timber.log.Timber
import java.util.Collections
-import javax.inject.Inject
/**
* Resolve homeserver base on search terms.
*/
-class HomeserverResolver @Inject constructor(
+@Inject
+class HomeserverResolver(
private val dispatchers: CoroutineDispatchers,
- private val wellknownRequest: WellknownRequest,
+ private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
) {
fun resolve(userInput: String): Flow> = flow {
val flowContext = currentCoroutineContext()
@@ -39,36 +39,24 @@ class HomeserverResolver @Inject constructor(
// Run all the requests in parallel
withContext(dispatchers.io) {
list.parallelMap { url ->
- val wellKnown = tryOrNull {
- withTimeout(5000) {
- wellknownRequest.execute(url)
- }
- }
- val isValid = wellKnown?.isValid().orFalse()
+ val isValid = homeServerLoginCompatibilityChecker.check(url)
+ .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
+ .getOrNull()
+ ?: return@parallelMap
+
+ // Emit the list as soon as possible
if (isValid) {
- // Emit the list as soon as possible
- currentList.add(
- HomeserverData(
- homeserverUrl = url,
- isWellknownValid = true,
- )
- )
+ currentList.add(HomeserverData(homeserverUrl = url))
withContext(flowContext) {
emit(currentList.toList())
}
}
}
}
- // If list is empty, and the user has entered an URL, do not block the user.
- if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) {
- emit(
- listOf(
- HomeserverData(
- homeserverUrl = trimmedUserInput,
- isWellknownValid = false,
- )
- )
- )
+ // If list is empty, and candidateBase is a valid an URL, do not block the user.
+ // A unsupported homeserver / homeserver not found error will be displayed if the user continues
+ if (currentList.isEmpty() && candidateBase.isValidUrl()) {
+ emit(listOf(HomeserverData(homeserverUrl = candidateBase)))
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt
deleted file mode 100644
index d217b4ca9e3..00000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-package io.element.android.features.login.impl.resolver.network
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.network.RetrofitFactory
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-class DefaultWellknownRequest @Inject constructor(
- private val retrofitFactory: RetrofitFactory,
-) : WellknownRequest {
- /**
- * Return the WellKnown data, if found.
- * @param baseUrl for instance https://matrix.org
- */
- override suspend fun execute(baseUrl: String): WellKnown {
- val wellknownApi = retrofitFactory.create(baseUrl)
- .create(WellknownAPI::class.java)
- return wellknownApi.getWellKnown()
- }
-}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt
deleted file mode 100644
index 2ed54bc4e25..00000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
- * Please see LICENSE files in the repository root for full details.
- */
-
-package io.element.android.features.login.impl.resolver.network
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * Example:
- *