Merge branch 'develop' into separate_import_error

This commit is contained in:
Hubert Chathi 2025-10-02 14:33:55 -04:00 committed by GitHub
commit 9a7b273beb
2773 changed files with 29051 additions and 10914 deletions

View file

@ -31,7 +31,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -39,7 +39,7 @@ jobs:
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -15,7 +15,7 @@ jobs:
if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name
steps: steps:
- name: Add auto-generated commit warning - name: Add auto-generated commit warning
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({

View file

@ -14,7 +14,7 @@ jobs:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -23,7 +23,7 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.13
- name: Run World screenshots generation script - name: Run World screenshots generation script

View file

@ -12,7 +12,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-java@v4 - uses: actions/setup-java@v5
name: Use JDK 21 name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:

View file

@ -28,7 +28,7 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }} ref: ${{ github.ref }}
- uses: actions/setup-java@v4 - uses: actions/setup-java@v5
name: Use JDK 21 name: Use JDK 21
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options

View file

@ -18,7 +18,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -21,7 +21,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -62,7 +62,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Trigger pipeline - name: Trigger pipeline
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }} github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }}
script: | script: |

View file

@ -15,7 +15,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: Add notice - name: Add notice
uses: actions/github-script@v7 uses: actions/github-script@v8
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with: with:
script: | script: |
@ -39,7 +39,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
- name: Add label - name: Add label
if: steps.teams.outputs.isTeamMember == 'false' if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
@ -58,7 +58,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- name: Close pull request - name: Close pull request
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({

View file

@ -35,7 +35,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.13
- name: Search for invalid screenshot files - name: Search for invalid screenshot files
@ -47,7 +47,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -56,7 +56,7 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.13
- name: Search for invalid dependencies - name: Search for invalid dependencies
@ -85,7 +85,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -125,7 +125,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -169,7 +169,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -209,7 +209,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -249,7 +249,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -34,7 +34,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -61,7 +61,7 @@ jobs:
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -89,7 +89,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -28,7 +28,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'

View file

@ -13,7 +13,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -22,7 +22,7 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.13
- name: Setup Localazy - name: Setup Localazy

View file

@ -14,7 +14,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.13 python-version: 3.13
- name: Install Prerequisite dependencies - name: Install Prerequisite dependencies

View file

@ -47,7 +47,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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 run: git submodule update --init --recursive
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@ -82,7 +82,7 @@ jobs:
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov - name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View file

@ -62,6 +62,7 @@ captures/
# Android Studio 3 in .gitignore file. # Android Studio 3 in .gitignore file.
.idea/caches .idea/caches
.idea/copilot .idea/copilot
.idea/copilot.*
.idea/inspectionProfiles .idea/inspectionProfiles
# Shelved changes in the IDE # Shelved changes in the IDE
.idea/shelf .idea/shelf

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.2.10" /> <option name="version" value="2.2.20" />
</component> </component>
</project> </project>

View file

@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard - hideKeyboard
- tapOn: "Continue" - tapOn: "Continue"
- extendedWaitUntil: - extendedWaitUntil:
visible: "Verification complete" visible: "Device verified"
timeout: 30000 timeout: 30000
- tapOn: "Continue" - tapOn: "Continue"

View file

@ -1,3 +1,200 @@
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 Changes in Element X v25.08.3
============================= =============================

View file

@ -8,7 +8,3 @@ plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
id("com.android.lint") id("com.android.lint")
} }
dependencies {
api(libs.inject)
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * 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 import kotlin.reflect.KClass
@ -13,7 +13,7 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph. * Adds Node to the specified component graph.
* Equivalent to the following declaration: * Equivalent to the following declaration:
* *
* @Module * @BindingContainer
* @ContributesTo(Scope::class) * @ContributesTo(Scope::class)
* abstract class YourNodeModule { * abstract class YourNodeModule {

View file

@ -1 +0,0 @@
io.element.android.anvilcodegen.ContributesNodeProcessorProvider

View file

@ -13,7 +13,6 @@ import com.android.build.gradle.tasks.GenerateBuildConfig
import com.google.firebase.appdistribution.gradle.firebaseAppDistribution import com.google.firebase.appdistribution.gradle.firebaseAppDistribution
import config.BuildTimeConfig import config.BuildTimeConfig
import extension.AssetCopyTask import extension.AssetCopyTask
import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource import extension.GitRevisionValueSource
import extension.allEnterpriseImpl import extension.allEnterpriseImpl
@ -23,8 +22,9 @@ import extension.allServicesImpl
import extension.buildConfigFieldStr import extension.buildConfigFieldStr
import extension.koverDependencies import extension.koverDependencies
import extension.locales import extension.locales
import extension.setupAnvil import extension.setupDependencyInjection
import extension.setupKover import extension.setupKover
import extension.testCommonDependencies
import java.util.Locale import java.util.Locale
plugins { plugins {
@ -37,7 +37,7 @@ plugins {
alias(libs.plugins.licensee) alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
// To be able to update the firebase.xml files, uncomment and build the project // 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() setupKover()
@ -103,7 +103,8 @@ android {
} }
val baseAppName = BuildTimeConfig.APPLICATION_NAME 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 { buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
@ -247,11 +248,7 @@ knit {
} }
} }
setupAnvil( setupDependencyInjection()
generateDaggerCode = true,
generateDaggerFactoriesUsingAnvil = false,
componentMergingStrategy = ComponentMergingStrategy.KSP,
)
dependencies { dependencies {
allLibrariesImpl() allLibrariesImpl()
@ -260,6 +257,7 @@ dependencies {
allEnterpriseImpl(project) allEnterpriseImpl(project)
implementation(projects.appicon.enterprise) implementation(projects.appicon.enterprise)
} else { } else {
implementation(projects.features.enterprise.implFoss)
implementation(projects.appicon.element) implementation(projects.appicon.element)
} }
allFeaturesImpl(project) allFeaturesImpl(project)
@ -293,12 +291,7 @@ dependencies {
implementation(libs.matrix.emojibase.bindings) implementation(libs.matrix.emojibase.bindings)
testImplementation(libs.test.junit) testCommonDependencies(libs)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)

View file

@ -19,6 +19,7 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -9,16 +9,16 @@ package io.element.android.x
import android.app.Application import android.app.Application
import androidx.startup.AppInitializer import androidx.startup.AppInitializer
import dev.zacsweers.metro.createGraphFactory
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.x.di.AppComponent import io.element.android.x.di.AppGraph
import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.PlatformInitializer import io.element.android.x.initializer.PlatformInitializer
class ElementXApplication : Application(), DaggerComponentOwner { class ElementXApplication : Application(), DependencyInjectionGraphOwner {
override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) override val graph: AppGraph = createGraphFactory<AppGraph.Factory>().create(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View file

@ -21,8 +21,8 @@ import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import io.element.android.appnav.RootFlowNode import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -38,8 +38,8 @@ class MainNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
), ),
DaggerComponentOwner { DependencyInjectionGraphOwner {
override val daggerComponent = (context as DaggerComponentOwner).daggerComponent override val graph = (context as DependencyInjectionGraphOwner).graph
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
return createNode<RootFlowNode>(buildContext = buildContext) return createNode<RootFlowNode>(buildContext = buildContext)

View file

@ -7,7 +7,8 @@
package io.element.android.x.di 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.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
@ -15,7 +16,6 @@ import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.platform.InitPlatformService import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService

View file

@ -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
}
}

View file

@ -0,0 +1,28 @@
/*
* 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 dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.annotations.ApplicationContext
@DependencyGraph(AppScope::class)
interface AppGraph : NodeFactoriesBindings {
val sessionGraphFactory: SessionGraph.Factory
@DependencyGraph.Factory
interface Factory {
fun create(
@ApplicationContext @Provides
context: Context
): AppGraph
}
}

View file

@ -11,9 +11,11 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.anvil.annotations.ContributesTo import dev.zacsweers.metro.AppScope
import dagger.Module import dev.zacsweers.metro.BindingContainer
import dagger.Provides 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.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.EnterpriseService 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.DefaultEmojibaseProvider
@ -23,24 +25,23 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.CacheDirectory 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.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import io.element.android.x.R import io.element.android.x.R
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import java.io.File import java.io.File
@Module @BindingContainer
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
object AppModule { object AppModule {
@Provides @Provides
@BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File { fun providesBaseDirectory(@ApplicationContext context: Context): File {
return File(context.filesDir, "sessions") return File(context.filesDir, "sessions")
} }
@ -105,11 +106,7 @@ object AppModule {
@Provides @Provides
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
fun providesCoroutineDispatchers(): CoroutineDispatchers { fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers( return CoroutineDispatchers.Default
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
)
} }
@Provides @Provides

View file

@ -7,20 +7,19 @@
package io.element.android.x.di package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.appnav.di.RoomComponentFactory import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import javax.inject.Inject
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultRoomComponentFactory @Inject constructor( @Inject
private val roomComponentBuilder: RoomComponent.Builder class DefaultRoomGraphFactory(
) : RoomComponentFactory { private val sessionGraph: SessionGraph,
) : RoomGraphFactory {
override fun create(room: JoinedRoom): Any { override fun create(room: JoinedRoom): Any {
return roomComponentBuilder return sessionGraph.roomGraphFactory
.joinedRoom(room) .create(room, room)
.baseRoom(room)
.build()
} }
} }

View file

@ -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()
}
}

View file

@ -0,0 +1,24 @@
/*
* 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 dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.libraries.matrix.api.MatrixClient
@ContributesBinding(AppScope::class)
@Inject
class DefaultSessionGraphFactory(
private val appGraph: AppGraph
) : SessionGraphFactory {
override fun create(client: MatrixClient): Any {
return appGraph.sessionGraphFactory.create(client)
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.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 {
@GraphExtension.Factory
interface Factory {
fun create(
@Provides joinedRoom: JoinedRoom,
@Provides baseRoom: BaseRoom
): RoomGraph
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,24 @@
/*
* 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
}
}

View file

@ -10,10 +10,14 @@ package io.element.android.x.initializer
import android.content.Context import android.content.Context
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler 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<Unit> { class CrashInitializer : Initializer<Unit> {
override fun create(context: Context) { override fun create(context: Context) {
VectorUncaughtExceptionHandler(context).activate() VectorUncaughtExceptionHandler(
context.bindings<RageshakeBindings>().preferencesCrashDataStore(),
).activate()
} }
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList() override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()

View file

@ -10,19 +10,20 @@ package io.element.android.x.intent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.net.toUri import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding import dev.zacsweers.metro.AppScope
import io.element.android.libraries.deeplink.DeepLinkCreator import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.AppScope import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId 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.ThreadId
import io.element.android.libraries.push.impl.intent.IntentProvider import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity import io.element.android.x.MainActivity
import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultIntentProvider @Inject constructor( @Inject
class DefaultIntentProvider(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val deepLinkCreator: DeepLinkCreator, private val deepLinkCreator: DeepLinkCreator,
) : IntentProvider { ) : IntentProvider {
@ -33,7 +34,7 @@ class DefaultIntentProvider @Inject constructor(
): Intent { ): Intent {
return Intent(context, MainActivity::class.java).apply { return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri() data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
} }
} }
} }

View file

@ -7,15 +7,16 @@
package io.element.android.x.oidc package io.element.android.x.oidc
import com.squareup.anvil.annotations.ContributesBinding import dev.zacsweers.metro.AppScope
import io.element.android.libraries.di.AppScope import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.x.R import io.element.android.x.R
import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultOidcRedirectUrlProvider @Inject constructor( @Inject
class DefaultOidcRedirectUrlProvider(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider { ) : OidcRedirectUrlProvider {
override fun provide() = buildString { override fun provide() = buildString {

View file

@ -9,6 +9,7 @@
<locale android:name="el"/> <locale android:name="el"/>
<locale android:name="en"/> <locale android:name="en"/>
<locale android:name="en_US"/> <locale android:name="en_US"/>
<locale android:name="eo"/>
<locale android:name="es"/> <locale android:name="es"/>
<locale android:name="et"/> <locale android:name="et"/>
<locale android:name="eu"/> <locale android:name="eu"/>
@ -19,6 +20,7 @@
<locale android:name="in"/> <locale android:name="in"/>
<locale android:name="it"/> <locale android:name="it"/>
<locale android:name="ka"/> <locale android:name="ka"/>
<locale android:name="ko"/>
<locale android:name="lt"/> <locale android:name="lt"/>
<locale android:name="nb"/> <locale android:name="nb"/>
<locale android:name="nl"/> <locale android:name="nl"/>

View file

@ -5,15 +5,22 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:Suppress("SameParameterValue")
package io.element.android.x.intent package io.element.android.x.intent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.google.common.truth.Truth.assertThat 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.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.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.x.MainActivity import io.element.android.x.MainActivity
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -23,45 +30,31 @@ import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class DefaultIntentProviderTest { class DefaultIntentProviderTest {
@Test @Test
fun `test getViewRoomIntent with Session`() { fun `test getViewRoomIntent with data`() {
val sut = createDefaultIntentProvider() val deepLinkCreator = lambdaRecorder<SessionId, RoomId?, ThreadId?, String> { _, _, _ -> "deepLinkCreatorResult" }
val result = sut.getViewRoomIntent( val sut = createDefaultIntentProvider(
sessionId = A_SESSION_ID, deepLinkCreator = { sessionId, roomId, threadId -> deepLinkCreator.invoke(sessionId, roomId, threadId) },
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,
)
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( val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
threadId = A_THREAD_ID, threadId = A_THREAD_ID,
) )
result.commonAssertions() 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),
)
} }
private fun createDefaultIntentProvider(): DefaultIntentProvider { private fun createDefaultIntentProvider(
deepLinkCreator: DeepLinkCreator = DeepLinkCreator { _, _, _ -> "" },
): DefaultIntentProvider {
return DefaultIntentProvider( return DefaultIntentProvider(
context = RuntimeEnvironment.getApplication() as Context, context = RuntimeEnvironment.getApplication() as Context,
deepLinkCreator = DeepLinkCreator(), deepLinkCreator = deepLinkCreator,
) )
} }

View file

@ -8,7 +8,8 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import extension.allFeaturesApi import extension.allFeaturesApi
import extension.setupAnvil import extension.setupDependencyInjection
import extension.testCommonDependencies
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
@ -19,15 +20,17 @@ android {
namespace = "io.element.android.appnav" namespace = "io.element.android.appnav"
} }
setupAnvil() setupDependencyInjection()
dependencies { dependencies {
allFeaturesApi(project) allFeaturesApi(project)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.accountselect.api)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) 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.matrix.api)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oidc.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
@ -35,6 +38,7 @@ dependencies {
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.features.login.api) implementation(projects.features.login.api)
@ -42,18 +46,12 @@ dependencies {
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.features.share.api) implementation(projects.features.share.api)
implementation(projects.features.viewfolder.api)
implementation(projects.services.apperror.impl) implementation(projects.services.apperror.impl)
implementation(projects.services.appnavstate.api) implementation(projects.services.appnavstate.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
testImplementation(libs.test.junit) testCommonDependencies(libs)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.oidc.test)
@ -61,11 +59,8 @@ dependencies {
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
testImplementation(libs.test.appyx.junit)
testImplementation(libs.test.arch.core)
} }

View file

@ -22,29 +22,30 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dev.zacsweers.metro.AppScope
import dagger.assisted.AssistedInject import dev.zacsweers.metro.Assisted
import io.element.android.anvilannotations.ContributesNode import dev.zacsweers.metro.AssistedInject
import io.element.android.appnav.di.SessionComponentFactory 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.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import kotlinx.parcelize.Parcelize 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]. * [io.element.android.libraries.di.SessionScope]. It has only one child: [LoggedInFlowNode].
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode]. * This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
*/ */
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class LoggedInAppScopeFlowNode @AssistedInject constructor( @AssistedInject
class LoggedInAppScopeFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
sessionComponentFactory: SessionComponentFactory, sessionGraphFactory: SessionGraphFactory,
private val imageLoaderHolder: ImageLoaderHolder, private val imageLoaderHolder: ImageLoaderHolder,
) : ParentNode<LoggedInAppScopeFlowNode.NavTarget>( ) : ParentNode<LoggedInAppScopeFlowNode.NavTarget>(
navModel = PermanentNavModel( navModel = PermanentNavModel(
@ -53,9 +54,10 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
), ),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
), DaggerComponentOwner { ), DependencyInjectionGraphOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
fun onAddAccount()
} }
@Parcelize @Parcelize
@ -66,7 +68,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
override val daggerComponent = sessionComponentFactory.create(inputs.matrixClient) override val graph = sessionGraphFactory.create(inputs.matrixClient)
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
@ -82,6 +84,10 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
override fun onOpenBugReport() { override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
override fun onAddAccount() {
plugins<Callback>().forEach { it.onAddAccount() }
}
} }
return createNode<LoggedInFlowNode>(buildContext, listOf(callback)) return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
} }

View file

@ -7,6 +7,7 @@
package io.element.android.appnav 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.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -18,9 +19,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class LoggedInEventProcessor @Inject constructor( @Inject
class LoggedInEventProcessor(
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
) { ) {
@ -30,9 +31,17 @@ class LoggedInEventProcessor @Inject constructor(
observingJob = roomMembershipObserver.updates observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom } .filter { !it.isUserInRoom }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach { roomMemberShipUpdate ->
when (it.change) { when (roomMemberShipUpdate.change) {
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room) 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.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock) MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
else -> Unit else -> Unit

View file

@ -36,10 +36,10 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom 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.LoggedInNode
import io.element.android.appnav.loggedin.MediaPreviewConfigMigration import io.element.android.appnav.loggedin.MediaPreviewConfigMigration
import io.element.android.appnav.loggedin.SendQueues import io.element.android.appnav.loggedin.SendQueues
@ -51,7 +51,6 @@ import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
@ -59,6 +58,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.features.startchat.api.StartChatEntryPoint import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@ -67,7 +67,6 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForNavTargetAttached import io.element.android.libraries.architecture.waitForNavTargetAttached
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -75,13 +74,13 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias 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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.ui.common.nodes.emptyNode
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -100,7 +99,8 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration import kotlin.time.toKotlinDuration
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class LoggedInFlowNode @AssistedInject constructor( @AssistedInject
class LoggedInFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val homeEntryPoint: HomeEntryPoint, private val homeEntryPoint: HomeEntryPoint,
@ -117,7 +117,6 @@ class LoggedInFlowNode @AssistedInject constructor(
private val shareEntryPoint: ShareEntryPoint, private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues, private val sendingQueue: SendQueues,
private val logoutEntryPoint: LogoutEntryPoint,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
@ -138,6 +137,7 @@ class LoggedInFlowNode @AssistedInject constructor(
) { ) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
fun onAddAccount()
} }
private val loggedInFlowProcessor = LoggedInEventProcessor( private val loggedInFlowProcessor = LoggedInEventProcessor(
@ -275,16 +275,13 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class IncomingShare(val intent: Intent) : NavTarget data class IncomingShare(val intent: Intent) : NavTarget
@Parcelize
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
@Parcelize @Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext) NavTarget.Placeholder -> emptyNode(buildContext)
NavTarget.LoggedInPermanent -> { NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback { val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() { override fun navigateToNotificationTroubleshoot() {
@ -322,10 +319,6 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onReportBugClick() { override fun onReportBugClick() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
}
} }
homeEntryPoint homeEntryPoint
.nodeBuilder(this, buildContext) .nodeBuilder(this, buildContext)
@ -333,7 +326,7 @@ class LoggedInFlowNode @AssistedInject constructor(
.build() .build()
} }
is NavTarget.Room -> { is NavTarget.Room -> {
val callback = object : JoinedRoomLoadedFlowNode.Callback { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) { override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
} }
@ -372,6 +365,11 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
} }
} }
val spaceCallback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters))
}
}
val inputs = RoomFlowNode.Inputs( val inputs = RoomFlowNode.Inputs(
roomIdOrAlias = navTarget.roomIdOrAlias, roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription), roomDescription = Optional.ofNullable(navTarget.roomDescription),
@ -379,7 +377,7 @@ class LoggedInFlowNode @AssistedInject constructor(
trigger = Optional.ofNullable(navTarget.trigger), trigger = Optional.ofNullable(navTarget.trigger),
initialElement = navTarget.initialElement initialElement = navTarget.initialElement
) )
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback)) createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback, spaceCallback))
} }
is NavTarget.UserProfile -> { is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback { val callback = object : UserProfileEntryPoint.Callback {
@ -394,6 +392,10 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
is NavTarget.Settings -> { is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback { val callback = object : PreferencesEntryPoint.Callback {
override fun onAddAccount() {
plugins<Callback>().forEach { it.onAddAccount() }
}
override fun onOpenBugReport() { override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
@ -406,11 +408,7 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
} }
override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) { override fun navigateTo(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))) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
} }
} }
@ -447,8 +445,7 @@ class LoggedInFlowNode @AssistedInject constructor(
.build() .build()
} }
NavTarget.Ftue -> { NavTarget.Ftue -> {
ftueEntryPoint.nodeBuilder(this, buildContext) ftueEntryPoint.createNode(this, buildContext)
.build()
} }
NavTarget.RoomDirectorySearch -> { NavTarget.RoomDirectorySearch -> {
roomDirectoryEntryPoint.nodeBuilder(this, buildContext) roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
@ -479,17 +476,6 @@ class LoggedInFlowNode @AssistedInject constructor(
.params(ShareEntryPoint.Params(intent = navTarget.intent)) .params(ShareEntryPoint.Params(intent = navTarget.intent))
.build() .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 -> { is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.nodeBuilder(this, buildContext) incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
.params(IncomingVerificationEntryPoint.Params(navTarget.data)) .params(IncomingVerificationEntryPoint.Params(navTarget.data))
@ -560,12 +546,6 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
} }
@ContributesNode(AppScope::class)
class PlaceholderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext, plugins = plugins)
} }
@Parcelize @Parcelize

View file

@ -20,9 +20,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import dagger.assisted.Assisted import dev.zacsweers.metro.AppScope
import dagger.assisted.AssistedInject import dev.zacsweers.metro.Assisted
import io.element.android.anvilannotations.ContributesNode 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.LoginEntryPoint
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
@ -31,12 +32,12 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation 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.NotLoggedInImageLoaderFactory
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class NotLoggedInFlowNode @AssistedInject constructor( @AssistedInject
class NotLoggedInFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,

View file

@ -9,24 +9,25 @@ package io.element.android.appnav
import android.content.Intent import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.animation.core.Spring
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node 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.Plugin
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
import dagger.assisted.AssistedInject 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 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.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.intent.ResolvedIntent
@ -38,43 +39,46 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.signedout.api.SignedOutEntryPoint 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.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode 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.createNode
import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.di.AppScope import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState 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.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class) @AssistedInject class RootFlowNode(
class RootFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val authenticationService: MatrixAuthenticationService, private val sessionStore: SessionStore,
private val accountProviderAccessControl: AccountProviderAccessControl, private val accountProviderAccessControl: AccountProviderAccessControl,
private val navStateFlowFactory: RootNavStateFlowFactory, private val navStateFlowFactory: RootNavStateFlowFactory,
private val matrixSessionCache: MatrixSessionCache, private val matrixSessionCache: MatrixSessionCache,
private val presenter: RootPresenter, private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint, private val bugReportEntryPoint: BugReportEntryPoint,
private val viewFolderEntryPoint: ViewFolderEntryPoint,
private val signedOutEntryPoint: SignedOutEntryPoint, private val signedOutEntryPoint: SignedOutEntryPoint,
private val accountSelectEntryPoint: AccountSelectEntryPoint,
private val intentResolver: IntentResolver, private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow, private val oidcActionFlow: OidcActionFlow,
private val bugReporter: BugReporter, private val bugReporter: BugReporter,
private val featureFlagService: FeatureFlagService,
) : BaseFlowNode<RootFlowNode.NavTarget>( ) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.SplashScreen, initialElement = NavTarget.SplashScreen,
@ -96,27 +100,24 @@ class RootFlowNode @AssistedInject constructor(
} }
private fun observeNavState() { private fun observeNavState() {
navStateFlowFactory.create(buildContext.savedStateMap) navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState ->
.distinctUntilChanged() Timber.v("navState=$navState")
.onEach { navState -> when (navState.loggedInState) {
Timber.v("navState=$navState") is LoggedInState.LoggedIn -> {
when (navState.loggedInState) { if (navState.loggedInState.isTokenValid) {
is LoggedInState.LoggedIn -> { tryToRestoreLatestSession(
if (navState.loggedInState.isTokenValid) { onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
tryToRestoreLatestSession( onFailure = { switchToNotLoggedInFlow(null) }
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, )
onFailure = { switchToNotLoggedInFlow(null) } } else {
) switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
} else {
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null)
} }
} }
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null)
}
} }
.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
} }
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
@ -138,22 +139,19 @@ class RootFlowNode @AssistedInject constructor(
onFailure: () -> Unit, onFailure: () -> Unit,
onSuccess: (SessionId) -> Unit, onSuccess: (SessionId) -> Unit,
) { ) {
matrixSessionCache.getOrRestore(sessionId) matrixSessionCache.getOrRestore(sessionId).onSuccess {
.onSuccess { Timber.v("Succeed to restore session $sessionId")
Timber.v("Succeed to restore session $sessionId") onSuccess(sessionId)
onSuccess(sessionId) }.onFailure {
} Timber.e(it, "Failed to restore session $sessionId")
.onFailure { onFailure()
Timber.e(it, "Failed to restore session $sessionId") }
onFailure()
}
} }
private suspend fun tryToRestoreLatestSession( private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit, onSuccess: (SessionId) -> Unit, onFailure: () -> Unit
onFailure: () -> Unit
) { ) {
val latestSessionId = authenticationService.getLatestSessionId() val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) { if (latestSessionId == null) {
onFailure() onFailure()
return return
@ -173,50 +171,63 @@ class RootFlowNode @AssistedInject constructor(
modifier = modifier, modifier = modifier,
onOpenBugReport = this::onOpenBugReport, onOpenBugReport = this::onOpenBugReport,
) { ) {
BackstackView() val backstackSlider = rememberBackstackSlider<NavTarget>(
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
)
val backstackFader = rememberBackstackFader<NavTarget>(
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
)
val transitionHandler = rememberDelegateTransitionHandler<NavTarget, BackStack.State> { navTarget ->
when (navTarget) {
is NavTarget.SplashScreen,
is NavTarget.LoggedInFlow -> backstackFader
else -> backstackSlider
}
}
BackstackView(transitionHandler = transitionHandler)
} }
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize data object SplashScreen : NavTarget
data object SplashScreen : NavTarget
@Parcelize @Parcelize data class AccountSelect(
data class NotLoggedInFlow( val currentSessionId: SessionId,
val intent: Intent?,
val permalinkData: PermalinkData?,
) : NavTarget
@Parcelize data class NotLoggedInFlow(
val params: LoginParams? val params: LoginParams?
) : NavTarget ) : NavTarget
@Parcelize @Parcelize data class LoggedInFlow(
data class LoggedInFlow( val sessionId: SessionId, val navId: Int
val sessionId: SessionId,
val navId: Int
) : NavTarget ) : NavTarget
@Parcelize @Parcelize data class SignedOutFlow(
data class SignedOutFlow(
val sessionId: SessionId val sessionId: SessionId
) : NavTarget ) : NavTarget
@Parcelize @Parcelize data object BugReport : NavTarget
data object BugReport : NavTarget
@Parcelize
data class ViewLogs(
val rootPath: String,
) : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId)
Timber.w("Couldn't find any session, go through SplashScreen") ?: return emptyNode(buildContext).also {
} Timber.w("Couldn't find any session, go through SplashScreen")
}
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
val callback = object : LoggedInAppScopeFlowNode.Callback { val callback = object : LoggedInAppScopeFlowNode.Callback {
override fun onOpenBugReport() { override fun onOpenBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
override fun onAddAccount() {
backstack.push(NavTarget.NotLoggedInFlow(null))
}
} }
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback)) createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
} }
@ -232,51 +243,46 @@ class RootFlowNode @AssistedInject constructor(
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback)) createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
} }
is NavTarget.SignedOutFlow -> { is NavTarget.SignedOutFlow -> {
signedOutEntryPoint.nodeBuilder(this, buildContext) signedOutEntryPoint.nodeBuilder(this, buildContext).params(
.params( SignedOutEntryPoint.Params(
SignedOutEntryPoint.Params( sessionId = navTarget.sessionId
sessionId = navTarget.sessionId
)
) )
.build() ).build()
} }
NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.SplashScreen -> emptyNode(buildContext)
NavTarget.BugReport -> { NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback { val callback = object : BugReportEntryPoint.Callback {
override fun onBugReportSent() {
backstack.pop()
}
override fun onViewLogs(basePath: String) {
backstack.push(NavTarget.ViewLogs(rootPath = basePath))
}
}
bugReportEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
is NavTarget.ViewLogs -> {
val callback = object : ViewFolderEntryPoint.Callback {
override fun onDone() { override fun onDone() {
backstack.pop() backstack.pop()
} }
} }
val params = ViewFolderEntryPoint.Params( bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
rootPath = navTarget.rootPath,
)
viewFolderEntryPoint
.nodeBuilder(this, buildContext)
.params(params)
.callback(callback)
.build()
} }
} is NavTarget.AccountSelect -> {
} val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
override fun onSelectAccount(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)
}
}
}
}
private fun splashNode(buildContext: BuildContext) = node(buildContext) { override fun onCancel() {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { backstack.pop()
CircularProgressIndicator() }
}
accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
}
} }
} }
@ -292,78 +298,129 @@ class RootFlowNode @AssistedInject constructor(
} }
private suspend fun onLoginLink(params: LoginParams) { private suspend fun onLoginLink(params: LoginParams) {
// Is there a session already? if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
val latestSessionId = authenticationService.getLatestSessionId() // Is there a session already?
if (latestSessionId == null) { val sessions = sessionStore.getAllSessions()
// No session, open login if (sessions.isNotEmpty()) {
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) { if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
switchToNotLoggedInFlow(params) 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 { } else {
Timber.w("Login link ignored, we are not allowed to connect to the homeserver") switchToNotLoggedInFlow(params)
switchToNotLoggedInFlow(null)
} }
} else { } else {
// Just ignore the login link if we already have a session Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
Timber.w("Login link ignored, we already have a session")
} }
} }
private suspend fun onIncomingShare(intent: Intent) { private suspend fun onIncomingShare(intent: Intent) {
// Is there a session already? // Is there a session already?
val latestSessionId = authenticationService.getLatestSessionId() val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) { if (latestSessionId == null) {
// No session, open login // No session, open login
switchToNotLoggedInFlow(null) switchToNotLoggedInFlow(null)
} else { } else {
attachSession(latestSessionId) // wait for the current session to be restored
.attachIncomingShare(intent) val loggedInFlowNode = attachSession(latestSessionId)
if (sessionStore.getAllSessions().size > 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) { private suspend fun navigateTo(permalinkData: PermalinkData) {
Timber.d("Navigating to $permalinkData") Timber.d("Navigating to $permalinkData")
attachSession(null) // Is there a session already?
.apply { val latestSessionId = sessionStore.getLatestSessionId()
when (permalinkData) { if (latestSessionId == null) {
is PermalinkData.FallbackLink -> Unit // No session, open login
is PermalinkData.RoomEmailInviteLink -> Unit switchToNotLoggedInFlow(null)
is PermalinkData.RoomLink -> { } else {
attachRoom( // wait for the current session to be restored
roomIdOrAlias = permalinkData.roomIdOrAlias, val loggedInFlowNode = attachSession(latestSessionId)
trigger = JoinedRoom.Trigger.MobilePermalink, when (permalinkData) {
serverNames = permalinkData.viaParameters, is PermalinkData.FallbackLink -> Unit
eventId = permalinkData.eventId, is PermalinkData.RoomEmailInviteLink -> Unit
clearBackstack = true else -> {
if (sessionStore.getAllSessions().size > 1) {
// Several accounts, let the user choose which one to use
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
intent = null,
permalinkData = permalinkData,
)
) )
} } else {
is PermalinkData.UserLink -> { // Only one account, directly attach the room or the user node.
attachUser(permalinkData.userId) loggedInFlowNode.attachPermalinkData(permalinkData)
} }
} }
} }
}
}
private suspend fun LoggedInFlowNode.attachPermalinkData(permalinkData: PermalinkData) {
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 PermalinkData.UserLink -> {
attachUser(permalinkData.userId)
}
}
} }
private suspend fun navigateTo(deeplinkData: DeeplinkData) { private suspend fun navigateTo(deeplinkData: DeeplinkData) {
Timber.d("Navigating to $deeplinkData") Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId) attachSession(deeplinkData.sessionId).apply {
.apply { when (deeplinkData) {
when (deeplinkData) { is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
}
} }
}
} }
private fun onOidcAction(oidcAction: OidcAction) { private fun onOidcAction(oidcAction: OidcAction) {
oidcActionFlow.post(oidcAction) oidcActionFlow.post(oidcAction)
} }
// [sessionId] will be null for permalink. private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode { // Ensure that the session is the latest one
// TODO handle multi-session sessionStore.setLatestSession(sessionId.value)
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget -> return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId) navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
} }.attachSession()
.attachSession()
} }
} }
private suspend fun SessionStore.getLatestSessionId() = getLatestSession()?.userId?.let(::SessionId)

View file

@ -10,9 +10,10 @@ package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.core.state.SavedStateMap
import com.squareup.anvil.annotations.ContributesBinding import dev.zacsweers.metro.AppScope
import io.element.android.libraries.di.AppScope import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.SingleIn import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -22,7 +23,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
@ -33,7 +33,8 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold
*/ */
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class MatrixSessionCache @Inject constructor( @Inject
class MatrixSessionCache(
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val syncOrchestratorFactory: SyncOrchestrator.Factory, private val syncOrchestratorFactory: SyncOrchestrator.Factory,
) : MatrixClientProvider { ) : MatrixClientProvider {

View file

@ -9,6 +9,6 @@ package io.element.android.appnav.di
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
interface RoomComponentFactory { fun interface RoomGraphFactory {
fun create(room: JoinedRoom): Any fun create(room: JoinedRoom): Any
} }

View file

@ -9,6 +9,6 @@ package io.element.android.appnav.di
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
interface SessionComponentFactory { interface SessionGraphFactory {
fun create(client: MatrixClient): Any fun create(client: MatrixClient): Any
} }

View file

@ -8,9 +8,9 @@
package io.element.android.appnav.di package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -30,7 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SyncOrchestrator @AssistedInject constructor( @AssistedInject
class SyncOrchestrator(
@Assisted matrixClient: MatrixClient, @Assisted matrixClient: MatrixClient,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,

View file

@ -8,16 +8,16 @@
package io.element.android.appnav.intent package io.element.android.appnav.intent
import android.content.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.LoginIntentResolver
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser 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.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcIntentResolver import io.element.android.libraries.oidc.api.OidcIntentResolver
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
sealed interface ResolvedIntent { sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
@ -27,7 +27,8 @@ sealed interface ResolvedIntent {
data class IncomingShare(val intent: Intent) : ResolvedIntent data class IncomingShare(val intent: Intent) : ResolvedIntent
} }
class IntentResolver @Inject constructor( @Inject
class IntentResolver(
private val deeplinkParser: DeeplinkParser, private val deeplinkParser: DeeplinkParser,
private val loginIntentResolver: LoginIntentResolver, private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver, private val oidcIntentResolver: OidcIntentResolver,

View file

@ -13,13 +13,14 @@ 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.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class LoggedInNode @AssistedInject constructor( @AssistedInject
class LoggedInNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val loggedInPresenter: LoggedInPresenter, private val loggedInPresenter: LoggedInPresenter,

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -42,11 +43,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag) private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag)
class LoggedInPresenter @Inject constructor( @Inject
class LoggedInPresenter(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val syncService: SyncService, private val syncService: SyncService,
private val pushService: PushService, private val pushService: PushService,

View file

@ -7,6 +7,7 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -14,13 +15,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
/** /**
* This migration is temporary, will be safe to remove after some time. * 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. * 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 mediaPreviewService: MediaPreviewService,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
@SessionCoroutineScope @SessionCoroutineScope

View file

@ -8,9 +8,10 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import androidx.annotation.VisibleForTesting 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.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.SessionScope 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.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
@ -21,13 +22,13 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
@VisibleForTesting @VisibleForTesting
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
class SendQueues @Inject constructor( @Inject
class SendQueues(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val syncService: SyncService, private val syncService: SyncService,
) { ) {

View file

@ -21,16 +21,18 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom 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.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.joined.LoadingRoomNodeView import io.element.android.appnav.room.joined.LoadingRoomNodeView
import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint 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.features.roomdirectory.api.RoomDescription
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
@ -52,7 +54,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
@ -63,7 +64,8 @@ import java.util.Optional
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class RoomFlowNode @AssistedInject constructor( @AssistedInject
class RoomFlowNode(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val client: MatrixClient, private val client: MatrixClient,
@ -71,6 +73,7 @@ class RoomFlowNode @AssistedInject constructor(
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val syncService: SyncService, private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver, private val membershipObserver: RoomMembershipObserver,
private val spaceEntryPoint: SpaceEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>( ) : BaseFlowNode<RoomFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Loading, initialElement = NavTarget.Loading,
@ -105,6 +108,9 @@ class RoomFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget data class JoinedRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class JoinedSpace(val spaceId: RoomId) : NavTarget
} }
override fun onBuilt() { override fun onBuilt() {
@ -142,40 +148,28 @@ class RoomFlowNode @AssistedInject constructor(
.withPreviousValue() .withPreviousValue()
combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace -> combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace ->
Timber.d("Room membership: $membership") Timber.d("Room membership: $membership")
when (membership) { if (membership == CurrentUserMembership.JOINED) {
CurrentUserMembership.JOINED -> { if (isSpace) {
if (isSpace) { backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId))
// It should not happen, but probably due to an issue in the sliding sync, } else {
// we can have a space here in case the space has just been joined. backstack.newRoot(NavTarget.JoinedRoom(roomId))
// 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 -> { } else {
if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) { val leavingFromCurrentDevice =
// The user left the room in this device, remove the room from the backstack membership == CurrentUserMembership.LEFT &&
if (!membershipUpdateFlow.first().isUserInRoom) { previousMembership == CurrentUserMembership.JOINED &&
navigateUp() membershipUpdateFlow.replayCache.lastOrNull()?.isUserInRoom == false
}
} else { if (leavingFromCurrentDevice) {
// Was invited or the room is not known, display the join room screen navigateUp()
backstack.newRoot( } else {
NavTarget.JoinRoom( backstack.newRoot(
roomId = roomId, NavTarget.JoinRoom(
serverNames = serverNames, roomId = roomId,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, serverNames = serverNames,
) trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
) )
} )
} }
} }
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
@ -193,7 +187,7 @@ class RoomFlowNode @AssistedInject constructor(
) )
} }
} }
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias) val params = Params(navTarget.roomAlias)
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext) roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
.callback(callback) .callback(callback)
.params(params) .params(params)
@ -217,6 +211,13 @@ class RoomFlowNode @AssistedInject constructor(
) )
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} }
is NavTarget.JoinedSpace -> {
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
spaceEntryPoint.nodeBuilder(this, buildContext)
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId))
.callback(spaceCallback)
.build()
}
} }
} }

View file

@ -24,9 +24,9 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
@ -45,7 +45,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class JoinedRoomFlowNode @AssistedInject constructor( @AssistedInject
class JoinedRoomFlowNode(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,

View file

@ -17,10 +17,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dev.zacsweers.metro.Assisted
import dagger.assisted.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.annotations.ContributesNode
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.RoomNavigationTarget
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
@ -28,7 +28,7 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -45,7 +45,8 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class JoinedRoomLoadedFlowNode @AssistedInject constructor( @AssistedInject
class JoinedRoomLoadedFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint, private val messagesEntryPoint: MessagesEntryPoint,
@ -55,7 +56,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
roomComponentFactory: RoomComponentFactory, roomGraphFactory: RoomGraphFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>( ) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) { initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
@ -67,7 +68,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
), ),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
), DaggerComponentOwner { ), DependencyInjectionGraphOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId, serverNames: List<String>) fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
@ -82,7 +83,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val callbacks = plugins.filterIsInstance<Callback>() private val callbacks = plugins.filterIsInstance<Callback>()
override val daggerComponent = roomComponentFactory.create(inputs.room) override val graph = roomGraphFactory.create(inputs.room)
init { init {
lifecycle.subscribe( lifecycle.subscribe(

View file

@ -9,18 +9,16 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.core.state.SavedStateMap
import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.MatrixSessionCache 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.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.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory 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.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY" private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY"
@ -28,12 +26,12 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact
* This class is responsible for creating a flow of [RootNavState]. * This class is responsible for creating a flow of [RootNavState].
* It gathers data from multiple datasource and creates a unique one. * It gathers data from multiple datasource and creates a unique one.
*/ */
class RootNavStateFlowFactory @Inject constructor( @Inject
private val authenticationService: MatrixAuthenticationService, class RootNavStateFlowFactory(
private val sessionStore: SessionStore,
private val cacheService: CacheService, private val cacheService: CacheService,
private val matrixSessionCache: MatrixSessionCache, private val matrixSessionCache: MatrixSessionCache,
private val imageLoaderHolder: ImageLoaderHolder, private val imageLoaderHolder: ImageLoaderHolder,
private val loginUserStory: LoginUserStory,
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory, private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
) { ) {
private var currentCacheIndex = 0 private var currentCacheIndex = 0
@ -41,14 +39,12 @@ class RootNavStateFlowFactory @Inject constructor(
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> { fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
return combine( return combine(
cacheIndexFlow(savedStateMap), cacheIndexFlow(savedStateMap),
authenticationService.loggedInStateFlow(), sessionStore.loggedInStateFlow(),
loginUserStory.loginFlowIsDone, ) { cacheIndex, loggedInState ->
) { cacheIndex, loggedInState, loginFlowIsDone -> RootNavState(
if (loginFlowIsDone) { cacheIndex = cacheIndex,
RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState) loggedInState = loggedInState,
} else { )
RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn)
}
} }
} }

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.SuperProperties
import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
@ -18,9 +19,9 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
import javax.inject.Inject
class RootPresenter @Inject constructor( @Inject
class RootPresenter(
private val crashDetectionPresenter: Presenter<CrashDetectionState>, private val crashDetectionPresenter: Presenter<CrashDetectionState>,
private val rageshakeDetectionPresenter: Presenter<RageshakeDetectionState>, private val rageshakeDetectionPresenter: Presenter<RageshakeDetectionState>,
private val appErrorStateService: AppErrorStateService, private val appErrorStateService: AppErrorStateService,

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%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."</string> <string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s unterstützt das alte Protokoll nicht mehr. Bitte melde dich ab und wieder an, um die App weiter nutzen zu können."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."</string>
</resources> </resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"로그아웃 및 업그레이드"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s 더 이상 이전 프로토콜을 지원하지 않습니다. 계속 사용하려면 로그아웃 후 다시 로그인해 주세요."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"귀하의 홈서버는 더 이상 이전 프로토콜을 지원하지 않습니다. 앱을 계속 사용하려면 로그아웃한 후 다시 로그인하세요."</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Sair e atualizar"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Sair e atualizar"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%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."</string> <string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s não tem mais suporte ao protocolo antigo. Saia da sua conta e entre novamente para continuar utilizando o aplicativo."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu servidor doméstico não é mais compatível com o protocolo antigo. Faça logout e login novamente para continuar usando o aplicativo."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu servidor-casa não é mais compatível com o protocolo antigo. Saia da sua conta e entre novamente para continuar usando o aplicativo."</string>
</resources> </resources>

View file

@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Sair &amp; Atualizar"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Sair &amp; Atualizar"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%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."</string> <string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%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."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"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."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"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."</string>
</resources> </resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Deconectați-vă și faceți upgrade"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Deconectați-vă și faceți upgrade"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%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."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"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."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"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."</string>
</resources> </resources>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">ıkış Yap ve Yükselt"</string> <string name="banner_migrate_to_native_sliding_sync_action">ıkış Yap ve Yükselt"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s artık eski protokolü destekleniyor. Uygulamayı kullanmaya devam etmek için lütfen çıkış yapın ve tekrar giriş yapın
"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ana sunucunuz artık eski protokolü desteklemiyor. Lütfen oturumu kapatın ve uygulamayı kullanmaya devam etmek için tekrar oturum açın."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ana sunucunuz artık eski protokolü desteklemiyor. Lütfen oturumu kapatın ve uygulamayı kullanmaya devam etmek için tekrar oturum açın."</string>
</resources> </resources>

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.di.RoomComponentFactory import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
@ -70,7 +70,7 @@ class JoinedRoomLoadedFlowNodeTest {
} }
} }
private class FakeRoomComponentFactory : RoomComponentFactory { private class FakeRoomGraphFactory : RoomGraphFactory {
override fun create(room: JoinedRoom): Any { override fun create(room: JoinedRoom): Any {
return Unit return Unit
} }
@ -110,7 +110,7 @@ class JoinedRoomLoadedFlowNodeTest {
roomDetailsEntryPoint = roomDetailsEntryPoint, roomDetailsEntryPoint = roomDetailsEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService = FakeAppNavigationStateService(),
sessionCoroutineScope = this, sessionCoroutineScope = this,
roomComponentFactory = FakeRoomComponentFactory(), roomGraphFactory = FakeRoomGraphFactory(),
matrixClient = FakeMatrixClient(), matrixClient = FakeMatrixClient(),
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
) )

View file

@ -14,9 +14,7 @@ import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -46,15 +44,11 @@ class IntentResolverTest {
@Test @Test
fun `test resolve navigation intent root`() { 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 { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID,
roomId = null,
threadId = null,
)
.toUri()
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
@ -68,15 +62,15 @@ class IntentResolverTest {
@Test @Test
fun `test resolve navigation intent room`() { fun `test resolve navigation intent room`() {
val sut = createIntentResolver() val sut = createIntentResolver(
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { deeplinkParserResult = DeeplinkData.Room(
action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
threadId = null, threadId = null,
) )
.toUri() )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
@ -92,15 +86,15 @@ class IntentResolverTest {
@Test @Test
fun `test resolve navigation intent thread`() { fun `test resolve navigation intent thread`() {
val sut = createIntentResolver() val sut = createIntentResolver(
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { deeplinkParserResult = DeeplinkData.Room(
action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
threadId = A_THREAD_ID, threadId = A_THREAD_ID,
) )
.toUri() )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
@ -117,7 +111,7 @@ class IntentResolverTest {
@Test @Test
fun `test resolve oidc`() { fun `test resolve oidc`() {
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { OidcAction.GoBack }, oidcIntentResolverResult = { OidcAction.GoBack() },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -126,7 +120,7 @@ class IntentResolverTest {
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
ResolvedIntent.Oidc( ResolvedIntent.Oidc(
oidcAction = OidcAction.GoBack oidcAction = OidcAction.GoBack()
) )
) )
} }
@ -240,12 +234,13 @@ class IntentResolverTest {
} }
private fun createIntentResolver( private fun createIntentResolver(
deeplinkParserResult: DeeplinkData? = null,
permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = DeeplinkParser(), deeplinkParser = { deeplinkParserResult },
loginIntentResolver = FakeLoginIntentResolver( loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult, parseResult = loginIntentResolverResult,
), ),

View file

@ -501,22 +501,16 @@ class LoggedInPresenterTest {
@Test @Test
fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest { fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest {
// The migration will be forced if: // The migration will be forced if the user is not using the native sliding sync
// - The user is not using the native sliding sync
// - The sliding sync proxy is no longer supported
// - The native sliding sync is supported
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) }, currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) },
) )
createLoggedInPresenter( createLoggedInPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue() assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
} }
} }

View file

@ -5,24 +5,15 @@
* Please see LICENSE files in the repository root for full details. * 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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("io.element.android-root") id("io.element.android-root")
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) 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.dependencycheck) apply false
alias(libs.plugins.dependencyanalysis) alias(libs.plugins.dependencyanalysis)
alias(libs.plugins.detekt) alias(libs.plugins.detekt)
@ -102,6 +93,8 @@ allprojects {
// Fix compilation warning for annotations // Fix compilation warning for annotations
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
freeCompilerArgs.add("-Xannotation-default-target=first-only") freeCompilerArgs.add("-Xannotation-default-target=first-only")
// Opt-in to context receivers
freeCompilerArgs.add("-Xcontext-parameters")
} }
} }
} }

View file

@ -10,11 +10,10 @@ plugins {
} }
dependencies { dependencies {
implementation(projects.anvilannotations) implementation(projects.annotations)
api(libs.anvil.compiler.api) implementation(libs.metro.runtime)
implementation(libs.anvil.compiler.utils) implementation(libs.kotlin.compiler)
implementation(libs.kotlinpoet) implementation(libs.kotlinpoet)
implementation(libs.dagger)
implementation(libs.ksp.plugin) implementation(libs.ksp.plugin)
implementation(libs.kotlinpoet.ksp) implementation(libs.kotlinpoet.ksp)
} }

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * 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.KspExperimental
import com.google.devtools.ksp.getConstructors import com.google.devtools.ksp.getConstructors
@ -19,7 +19,6 @@ import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate import com.google.devtools.ksp.validate
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FileSpec
@ -30,13 +29,14 @@ import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dagger.Binds import dev.zacsweers.metro.Assisted
import dagger.Module import dev.zacsweers.metro.AssistedFactory
import dagger.assisted.Assisted import dev.zacsweers.metro.BindingContainer
import dagger.assisted.AssistedFactory import dev.zacsweers.metro.Binds
import dagger.assisted.AssistedInject import dev.zacsweers.metro.ContributesTo
import dagger.multibindings.IntoMap import dev.zacsweers.metro.IntoMap
import io.element.android.anvilannotations.ContributesNode import dev.zacsweers.metro.Origin
import io.element.android.annotations.ContributesNode
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
class ContributesNodeProcessor( class ContributesNodeProcessor(
@ -72,15 +72,16 @@ class ContributesNodeProcessor(
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
val modulePackage = ksClass.packageName.asString() val modulePackage = ksClass.packageName.asString()
val moduleClassName = "${ksClass.simpleName.asString()}_Module" val moduleClassName = "${ksClass.simpleName.asString()}_Module"
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val content = FileSpec.builder( val content = FileSpec.builder(
packageName = modulePackage, packageName = modulePackage,
fileName = moduleClassName, fileName = moduleClassName,
) )
.addType( .addType(
TypeSpec.classBuilder(moduleClassName) TypeSpec.interfaceBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT) .addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
.addAnnotation(Module::class) .addAnnotation(BindingContainer::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build()) .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
.addFunction( .addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory") FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
.addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.ABSTRACT)
@ -90,7 +91,7 @@ class ContributesNodeProcessor(
.addAnnotation(IntoMap::class) .addAnnotation(IntoMap::class)
.addAnnotation( .addAnnotation(
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember( AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
"%T::class", CLASS_PLACEHOLDER,
ClassName.bestGuess(ksClass.qualifiedName!!.asString()) ClassName.bestGuess(ksClass.qualifiedName!!.asString())
).build() ).build()
) )
@ -103,7 +104,7 @@ class ContributesNodeProcessor(
content.writeTo( content.writeTo(
codeGenerator = codeGenerator, codeGenerator = codeGenerator,
dependencies = Dependencies( dependencies = Dependencies(
aggregating = true, aggregating = false,
ksClass.containingFile!! ksClass.containingFile!!
), ),
) )
@ -113,23 +114,23 @@ class ContributesNodeProcessor(
private fun generateFactory(ksClass: KSClassDeclaration) { private fun generateFactory(ksClass: KSClassDeclaration) {
val generatedPackage = ksClass.packageName.asString() val generatedPackage = ksClass.packageName.asString()
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory" val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) } val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty() val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
if (constructor == null || assistedParameters.size != 2) { if (assistedParameters.size != 2) {
error( 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] val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name?.asString() != "buildContext") { if (contextAssistedParam.name?.asString() != "buildContext") {
error( error(
"${ksClass.qualifiedName} @Assisted parameter must be named buildContext", "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named buildContext",
) )
} }
val pluginsAssistedParam = assistedParameters[1] val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name?.asString() != "plugins") { if (pluginsAssistedParam.name?.asString() != "plugins") {
error( error(
"${ksClass.qualifiedName} @Assisted parameter must be named plugins", "${ksClass.qualifiedName?.asString()} @Assisted parameter must be named plugins",
) )
} }
@ -140,6 +141,7 @@ class ContributesNodeProcessor(
.addType( .addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName) TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName)) .addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember("%T::class", nodeClassName).build())
.addAnnotation(AssistedFactory::class) .addAnnotation(AssistedFactory::class)
.addFunction( .addFunction(
FunSpec.builder("create") FunSpec.builder("create")
@ -156,13 +158,14 @@ class ContributesNodeProcessor(
content.writeTo( content.writeTo(
codeGenerator = codeGenerator, codeGenerator = codeGenerator,
dependencies = Dependencies( dependencies = Dependencies(
aggregating = true, aggregating = false,
ksClass.containingFile!! ksClass.containingFile!!
), ),
) )
} }
companion object { companion object {
private const val CLASS_PLACEHOLDER = "%T::class"
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory") private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey") private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
} }

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * 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.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorEnvironment

View file

@ -0,0 +1 @@
io.element.android.codegen.ContributesNodeProcessorProvider

View file

@ -249,8 +249,7 @@ Main libraries and frameworks used in this application:
- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please - 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! 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 - Dependency injection: [Metro](https://zacsweers.github.io/metro/latest/)
watch [this video](https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/) to learn more about Anvil!
- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule) - Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule)
Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/)
@ -261,7 +260,7 @@ Here are the main points:
2. Views are compose first 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. 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`. 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. 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`. 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. 8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed.
@ -423,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 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 - 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!) 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! ## Happy coding!

View file

@ -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.

@ -1 +1 @@
Subproject commit 76e10f6fa4db4196df245a3d29131a95d9e60a4d Subproject commit 95789d40119499eba8a79284df9dd2306405b099

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -9,4 +9,4 @@ package io.element.android.features.analytics.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface AnalyticsEntryPoint : SimpleFeatureEntryPoint fun interface AnalyticsEntryPoint : SimpleFeatureEntryPoint

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string> <string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_settings_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string> <string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string> <string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string> <string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
</resources> </resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오."</string>
<string name="screen_analytics_settings_read_terms">"모든 이용 약관은 %1$s 에서 확인하실 수 있습니다."</string>
<string name="screen_analytics_settings_read_terms_content_link">"여기"</string>
<string name="screen_analytics_settings_share_data">"분석 데이터 공유"</string>
</resources>

View file

@ -3,5 +3,5 @@
<string name="screen_analytics_settings_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string> <string name="screen_analytics_settings_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s."</string> <string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string> <string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_settings_share_data">"Compartilhar dados de utilização"</string> <string name="screen_analytics_settings_share_data">"Compartilhar dados analíticos"</string>
</resources> </resources>

View file

@ -1,4 +1,5 @@
import extension.setupAnvil import extension.setupDependencyInjection
import extension.testCommonDependencies
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.
@ -16,7 +17,7 @@ android {
namespace = "io.element.android.features.analytics.impl" namespace = "io.element.android.features.analytics.impl"
} }
setupAnvil() setupDependencyInjection()
dependencies { dependencies {
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
@ -30,13 +31,7 @@ dependencies {
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
testImplementation(libs.test.junit) testCommonDependencies(libs)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils)
} }

View file

@ -14,16 +14,17 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dev.zacsweers.metro.AppScope
import dagger.assisted.AssistedInject import dev.zacsweers.metro.Assisted
import io.element.android.anvilannotations.ContributesNode import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.AnalyticsConfig import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class AnalyticsOptInNode @AssistedInject constructor( @AssistedInject
class AnalyticsOptInNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: AnalyticsOptInPresenter, private val presenter: AnalyticsOptInPresenter,

View file

@ -9,6 +9,7 @@ package io.element.android.features.analytics.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -16,9 +17,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class AnalyticsOptInPresenter @Inject constructor( @Inject
class AnalyticsOptInPresenter(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : Presenter<AnalyticsOptInState> { ) : Presenter<AnalyticsOptInState> {

View file

@ -8,9 +8,8 @@
package io.element.android.features.analytics.impl package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import javax.inject.Inject
open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterProvider<AnalyticsOptInState> { open class AnalyticsOptInStateProvider : PreviewParameterProvider<AnalyticsOptInState> {
override val values: Sequence<AnalyticsOptInState> override val values: Sequence<AnalyticsOptInState>
get() = sequenceOf( get() = sequenceOf(
aAnalyticsOptInState(), aAnalyticsOptInState(),

View file

@ -9,14 +9,15 @@ package io.element.android.features.analytics.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultAnalyticsEntryPoint @Inject constructor() : AnalyticsEntryPoint { @Inject
class DefaultAnalyticsEntryPoint : AnalyticsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node { override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<AnalyticsOptInNode>(buildContext) return parentNode.createNode<AnalyticsOptInNode>(buildContext)
} }

View file

@ -7,16 +7,16 @@
package io.element.android.features.analytics.impl.di package io.element.android.features.analytics.impl.di
import com.squareup.anvil.annotations.ContributesTo import dev.zacsweers.metro.AppScope
import dagger.Binds import dev.zacsweers.metro.BindingContainer
import dagger.Module 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.api.preferences.AnalyticsPreferencesState
import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@Module @BindingContainer
interface AnalyticsModule { interface AnalyticsModule {
@Binds @Binds
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState> fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>

View file

@ -10,6 +10,7 @@ package io.element.android.features.analytics.impl.preferences
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
@ -18,9 +19,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class AnalyticsPreferencesPresenter @Inject constructor( @Inject
class AnalyticsPreferencesPresenter(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : Presenter<AnalyticsPreferencesState> { ) : Presenter<AnalyticsPreferencesState> {

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string> <string name="screen_analytics_prompt_data_usage">"Wir speichern oder profilieren keine personenbezogenen Daten."</string>
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string> <string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string> <string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string> <string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Sie können dies jederzeit beenden"</string> <string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string> <string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
<string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string> <string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string>
</resources> </resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"개인 데이터는 기록하거나 프로파일링하지 않습니다."</string>
<string name="screen_analytics_prompt_help_us_improve">"익명화된 사용 데이터를 공유하여 문제점을 파악하는 데 도움을 주십시오."</string>
<string name="screen_analytics_prompt_read_terms">"모든 이용 약관은 %1$s 에서 확인하실 수 있습니다."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"여기"</string>
<string name="screen_analytics_prompt_settings">"이 기능을 언제든지 비활성화할 수 있습니다."</string>
<string name="screen_analytics_prompt_third_party_sharing">"우리는 귀하의 데이터를 제3자와 공유하지 않습니다."</string>
<string name="screen_analytics_prompt_title">"%1$s 개선하기"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more