Merge branch 'release/25.12.0'
This commit is contained in:
commit
386bd11156
503 changed files with 6837 additions and 3111 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
|
|||
2
.github/workflows/build_enterprise.yml
vendored
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-enterprise-{0}-{1}', matrix.variant, github.sha) || format('build-enterprise-{0}-{1}', matrix.variant, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
|
|||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
# Skip in forks, it doesn't work even with the fallback token
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/gradle-wrapper-update.yml
vendored
2
.github/workflows/gradle-wrapper-update.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
# Skip in forks
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
name: Use JDK 21
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||
|
|
|
|||
4
.github/workflows/maestro-local.yml
vendored
4
.github/workflows/maestro-local.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
group: ${{ format('maestro-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
group: ${{ format('maestro-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
|
|
|
|||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -60,7 +60,7 @@ jobs:
|
|||
name: Dependency analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
|
|||
22
.github/workflows/quality.yml
vendored
22
.github/workflows/quality.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Search for forbidden patterns
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
name: Search for invalid screenshot files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
name: Search for invalid dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
@ -71,7 +71,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -111,7 +111,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -144,7 +144,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -188,7 +188,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -228,7 +228,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -268,7 +268,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -299,7 +299,7 @@ jobs:
|
|||
name: Check shell scripts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run shellcheck
|
||||
uses: ludeeus/action-shellcheck@2.0.0
|
||||
with:
|
||||
|
|
@ -311,7 +311,7 @@ jobs:
|
|||
needs: [konsist, lint, ktlint, detekt]
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
|
|||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
|
|
@ -87,7 +87,7 @@ jobs:
|
|||
group: ${{ format('build-release-main-fdroid-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
group: ${{ format('sonar-{0}', github.ref) }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
|
|||
4
.github/workflows/sync-localazy.yml
vendored
4
.github/workflows/sync-localazy.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
# Skip in forks
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||
./tools/test/generateAllScreenshots.py
|
||||
- name: Create Pull Request for Strings
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
commit-message: Sync Strings from Localazy
|
||||
|
|
|
|||
4
.github/workflows/sync-sas-strings.yml
vendored
4
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
# No concurrency required, runs every time on a schedule.
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
- name: Run SAS String script
|
||||
run: ./tools/sas/import_sas_strings.py
|
||||
- name: Create Pull Request for SAS Strings
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
commit-message: Sync SAS Strings
|
||||
title: Sync SAS Strings
|
||||
|
|
|
|||
80
CHANGES.md
80
CHANGES.md
|
|
@ -1,3 +1,83 @@
|
|||
Changes in Element X v25.11.3
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v25.11.3 -->
|
||||
|
||||
## What's Changed
|
||||
### 🙌 Improvements
|
||||
* Improve rendering notification for multi account by @bmarty in https://github.com/element-hq/element-x-android/pull/5645
|
||||
* Change : roles and permissions by @ganfra in https://github.com/element-hq/element-x-android/pull/5685
|
||||
* Improve account provider selection during the login flow by @bmarty in https://github.com/element-hq/element-x-android/pull/5692
|
||||
* Let notifications use avatar fallback. by @bmarty in https://github.com/element-hq/element-x-android/pull/5721
|
||||
* Changes : member list improvements by @ganfra in https://github.com/element-hq/element-x-android/pull/5728
|
||||
### 🐛 Bugfixes
|
||||
* Do not use the bestDescription but the caption for images, when available by @bmarty in https://github.com/element-hq/element-x-android/pull/5684
|
||||
* Add the user certificate if any when creating Matrix Client. by @bmarty in https://github.com/element-hq/element-x-android/pull/5686
|
||||
* Ensure the form data are not lost when opening the log viewer. by @bmarty in https://github.com/element-hq/element-x-android/pull/5695
|
||||
* Fix password flow when using a login link by @bmarty in https://github.com/element-hq/element-x-android/pull/5693
|
||||
* Fix layout issue in text composer by @bmarty in https://github.com/element-hq/element-x-android/pull/5710
|
||||
* Fix navigation stack overflow when sharing media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5724
|
||||
* Notification robustness by @bmarty in https://github.com/element-hq/element-x-android/pull/5726
|
||||
* Send read receipts using the current timeline, not the live timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5731
|
||||
* Render Owner in the horizontal list when editing Admins. by @bmarty in https://github.com/element-hq/element-x-android/pull/5736
|
||||
* Stop overriding the homeserver when restoring a `Client` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5753
|
||||
* Revert "Stop overriding the homeserver when restoring a `Client`" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5754
|
||||
* Try fixing forced dark mode issues on MIUI on Android 10 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5708
|
||||
* Fix crash at startup by @bmarty in https://github.com/element-hq/element-x-android/pull/5761
|
||||
* Fix null pointer exception on room notification settings. by @bmarty in https://github.com/element-hq/element-x-android/pull/5758
|
||||
* Fix crash when viewing Pinned events by @bmarty in https://github.com/element-hq/element-x-android/pull/5764
|
||||
* Fix crash when pressing back from the showkase Activity by @bmarty in https://github.com/element-hq/element-x-android/pull/5772
|
||||
* Fix navigation issue once incoming share is handled by @bmarty in https://github.com/element-hq/element-x-android/pull/5773
|
||||
* Fix crash in work manager by @bmarty in https://github.com/element-hq/element-x-android/pull/5768
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5704
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5747
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5782
|
||||
### 🧱 Build
|
||||
* Module cleanup by @bmarty in https://github.com/element-hq/element-x-android/pull/5722
|
||||
* Add `NIGHTLY` env for Sentry by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5779
|
||||
### 🚧 In development 🚧
|
||||
* Space : prepare Space Settings screen by @ganfra in https://github.com/element-hq/element-x-android/pull/5668
|
||||
### Dependency upgrades
|
||||
* fix(deps): update dependency androidx.core:core-splashscreen to v1.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5687
|
||||
* fix(deps): update dependency com.posthog:posthog-android to v3.26.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5696
|
||||
* fix(deps): update metro to v0.7.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5697
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v25.11.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5716
|
||||
* Update plugin ktlint to v14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5713
|
||||
* Update plugin dependencycheck to v12.1.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5717
|
||||
* Update dependency org.maplibre.gl:android-sdk to v12.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5714
|
||||
* Update dependency io.sentry:sentry-android to v8.26.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5720
|
||||
* Update sqldelight to v2.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5730
|
||||
* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5746
|
||||
* fix(deps): update dependency com.google.firebase:firebase-bom to v34.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5737
|
||||
* fix(deps): update metro to v0.7.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5752
|
||||
* fix(deps): update dependency org.maplibre.gl:android-sdk to v12.1.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5743
|
||||
* Update dependency com.squareup.okhttp3:okhttp-bom to v5.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5757
|
||||
* fix(deps): update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5738
|
||||
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.11.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5762
|
||||
* fix(deps): update dependencyanalysis to v3.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5776
|
||||
### Others
|
||||
* Extract save change dialog by @bmarty in https://github.com/element-hq/element-x-android/pull/5679
|
||||
* Use the dedicated subdomain for the bug report URL by default by @benbz in https://github.com/element-hq/element-x-android/pull/5689
|
||||
* Convert `ComposerAlertMolecule` to use alert levels. by @kaylendog in https://github.com/element-hq/element-x-android/pull/5691
|
||||
* Improve composer alert molecule by @bmarty in https://github.com/element-hq/element-x-android/pull/5701
|
||||
* Code consistency around view event handling by @bmarty in https://github.com/element-hq/element-x-android/pull/5698
|
||||
* Update copyright holders by @bmarty in https://github.com/element-hq/element-x-android/pull/5706
|
||||
* Fix rendering notifications after receiving redundant push by @SpiritCroc in https://github.com/element-hq/element-x-android/pull/5711
|
||||
* Fix push gateway with some push provider (Sunup/autopush) by @p1gp1g in https://github.com/element-hq/element-x-android/pull/5741
|
||||
* Use new notification sound in release. by @bmarty in https://github.com/element-hq/element-x-android/pull/5748
|
||||
* Fix issue on brand color override by @bmarty in https://github.com/element-hq/element-x-android/pull/5626
|
||||
* Add media retention policy by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5749
|
||||
* Enable logging OkHttp traffic based on the current log level by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5750
|
||||
* Remove unused `slidingSyncProxy` from DB. by @bmarty in https://github.com/element-hq/element-x-android/pull/5755
|
||||
* Add some performance metrics for Sentry by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5760
|
||||
|
||||
## New Contributors
|
||||
* @benbz made their first contribution in https://github.com/element-hq/element-x-android/pull/5689
|
||||
* @kaylendog made their first contribution in https://github.com/element-hq/element-x-android/pull/5691
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.11.2...v25.11.3
|
||||
|
||||
Changes in Element X v25.11.2
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -129,12 +129,12 @@ android {
|
|||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
postprocessing {
|
||||
isRemoveUnusedCode = true
|
||||
isObfuscate = false
|
||||
isOptimizeCode = true
|
||||
isRemoveUnusedResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
optimization {
|
||||
enable = true
|
||||
keepRules {
|
||||
files.add(File(projectDir, "proguard-rules.pro"))
|
||||
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,10 +152,6 @@ android {
|
|||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs.getByName("nightly")
|
||||
|
||||
postprocessing {
|
||||
initWith(release.postprocessing)
|
||||
}
|
||||
|
||||
firebaseAppDistribution {
|
||||
artifactType = "APK"
|
||||
// We upload the universal APK to fix this error:
|
||||
|
|
@ -341,7 +337,7 @@ fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
|
|||
it.toString()
|
||||
}
|
||||
}
|
||||
val artifactsFile = reportingExtension.file("licensee/android$capitalizedVariantName/artifacts.json")
|
||||
val artifactsFile = reportingExtension.baseDirectory.file("licensee/android$capitalizedVariantName/artifacts.json")
|
||||
|
||||
val copyArtifactsTask =
|
||||
project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
|
||||
|
|
|
|||
23
app/proguard-rules.pro
vendored
23
app/proguard-rules.pro
vendored
|
|
@ -41,8 +41,6 @@
|
|||
static int windowAttachCount(android.view.View);
|
||||
}
|
||||
|
||||
-keep class io.element.android.x.di.** { *; }
|
||||
|
||||
|
||||
# Keep LogSessionId class and related classes (https://github.com/androidx/media/issues/2535)
|
||||
-keep class android.media.metrics.LogSessionId { *; }
|
||||
|
|
@ -51,3 +49,24 @@
|
|||
# Keep Media3 classes that use reflection (https://github.com/androidx/media/issues/2535)
|
||||
-keep class androidx.media3.** { *; }
|
||||
-dontwarn android.media.metrics.**
|
||||
|
||||
# New rules after AGP 8.13.1 upgrade
|
||||
-dontwarn androidx.window.extensions.WindowExtensions
|
||||
-dontwarn androidx.window.extensions.WindowExtensionsProvider
|
||||
-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation
|
||||
-dontwarn androidx.window.extensions.layout.DisplayFeature
|
||||
-dontwarn androidx.window.extensions.layout.FoldingFeature
|
||||
-dontwarn androidx.window.extensions.layout.WindowLayoutComponent
|
||||
-dontwarn androidx.window.extensions.layout.WindowLayoutInfo
|
||||
-dontwarn androidx.window.sidecar.SidecarDeviceState
|
||||
-dontwarn androidx.window.sidecar.SidecarDisplayFeature
|
||||
-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback
|
||||
-dontwarn androidx.window.sidecar.SidecarInterface
|
||||
-dontwarn androidx.window.sidecar.SidecarProvider
|
||||
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
||||
|
||||
# Also needed after AGP 8.13.1 upgrade, it seems like proguard is now more aggressive on removing unused code
|
||||
-keep class org.matrix.rustcomponents.sdk.** { *;}
|
||||
-keep class uniffi.** { *;}
|
||||
-keep class io.element.android.x.di.** { *; }
|
||||
-keepnames class io.element.android.x.**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.x.intent
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
|
@ -32,10 +33,12 @@ class DefaultIntentProvider(
|
|||
roomId: RoomId?,
|
||||
threadId: ThreadId?,
|
||||
eventId: EventId?,
|
||||
extras: Bundle?,
|
||||
): Intent {
|
||||
return Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = deepLinkCreator.create(sessionId, roomId, threadId, eventId).toUri()
|
||||
extras?.let(::putExtras)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import com.bumble.appyx.navmodel.backstack.operation.replace
|
|||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
import io.element.android.appnav.loggedin.MediaPreviewConfigMigration
|
||||
|
|
@ -84,6 +83,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
|
@ -92,6 +92,7 @@ import io.element.android.libraries.push.api.notifications.conversations.Notific
|
|||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsRoomListStateWatcher
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -108,6 +109,7 @@ import java.util.UUID
|
|||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toKotlinDuration
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
|
|
@ -139,6 +141,7 @@ class LoggedInFlowNode(
|
|||
private val buildMeta: BuildMeta,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Placeholder,
|
||||
|
|
@ -202,6 +205,7 @@ class LoggedInFlowNode(
|
|||
}
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
analyticsRoomListStateWatcher.start()
|
||||
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||
// TODO We do not support Space yet, so directly navigate to main space
|
||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||
|
|
@ -238,6 +242,7 @@ class LoggedInFlowNode(
|
|||
appNavigationStateService.onLeavingSession(id)
|
||||
loggedInFlowProcessor.stopObserving()
|
||||
matrixClient.sessionVerificationService.setListener(null)
|
||||
analyticsRoomListStateWatcher.stop()
|
||||
}
|
||||
)
|
||||
setupSendingQueue()
|
||||
|
|
@ -261,7 +266,7 @@ class LoggedInFlowNode(
|
|||
data class Room(
|
||||
val roomIdOrAlias: RoomIdOrAlias,
|
||||
val serverNames: List<String> = emptyList(),
|
||||
val trigger: JoinedRoom.Trigger? = null,
|
||||
val trigger: JoinedRoomAnalyticsEvent.Trigger? = null,
|
||||
val roomDescription: RoomDescription? = null,
|
||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
|
||||
val targetId: UUID = UUID.randomUUID(),
|
||||
|
|
@ -311,8 +316,13 @@ class LoggedInFlowNode(
|
|||
}
|
||||
NavTarget.Home -> {
|
||||
val callback = object : HomeEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
|
||||
backstack.push(
|
||||
NavTarget.Room(
|
||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToSettings() {
|
||||
|
|
@ -361,7 +371,7 @@ class LoggedInFlowNode(
|
|||
val target = NavTarget.Room(
|
||||
roomIdOrAlias = data.roomIdOrAlias,
|
||||
serverNames = data.viaParameters,
|
||||
trigger = JoinedRoom.Trigger.Timeline,
|
||||
trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline,
|
||||
initialElement = RoomNavigationTarget.Root(data.eventId),
|
||||
)
|
||||
if (pushToBackstack) {
|
||||
|
|
@ -475,7 +485,7 @@ class LoggedInFlowNode(
|
|||
NavTarget.Room(
|
||||
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
|
||||
roomDescription = roomDescription,
|
||||
trigger = JoinedRoom.Trigger.RoomDirectory,
|
||||
trigger = JoinedRoomAnalyticsEvent.Trigger.RoomDirectory,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -515,7 +525,7 @@ class LoggedInFlowNode(
|
|||
suspend fun attachRoom(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
serverNames: List<String> = emptyList(),
|
||||
trigger: JoinedRoom.Trigger? = null,
|
||||
trigger: JoinedRoomAnalyticsEvent.Trigger? = null,
|
||||
eventId: EventId? = null,
|
||||
clearBackstack: Boolean,
|
||||
): RoomFlowNode {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
|
||||
import io.element.android.libraries.designsystem.utils.ScreenOrientation
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
|
|
@ -43,6 +44,7 @@ class NotLoggedInFlowNode(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val loginEntryPoint: LoginEntryPoint,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
|
||||
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -57,6 +59,7 @@ class NotLoggedInFlowNode(
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToBugReport()
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
|
@ -64,6 +67,7 @@ class NotLoggedInFlowNode(
|
|||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
analyticsColdStartWatcher.whenLoggingIn()
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
|
||||
|
|
@ -83,6 +87,10 @@ class NotLoggedInFlowNode(
|
|||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun onDone() {
|
||||
callback.onDone()
|
||||
}
|
||||
}
|
||||
loginEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ import io.element.android.libraries.oidc.api.OidcActionFlow
|
|||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||
import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -86,6 +90,8 @@ class RootFlowNode(
|
|||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
|
||||
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
|
|
@ -95,6 +101,7 @@ class RootFlowNode(
|
|||
plugins = plugins
|
||||
) {
|
||||
override fun onBuilt() {
|
||||
analyticsColdStartWatcher.start()
|
||||
matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
|
||||
super.onBuilt()
|
||||
observeNavState()
|
||||
|
|
@ -243,6 +250,10 @@ class RootFlowNode(
|
|||
override fun navigateToBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
val params = NotLoggedInFlowNode.Params(
|
||||
loginParams = navTarget.params,
|
||||
|
|
@ -306,7 +317,13 @@ class RootFlowNode(
|
|||
suspend fun handleIntent(intent: Intent) {
|
||||
val resolvedIntent = intentResolver.resolve(intent) ?: return
|
||||
when (resolvedIntent) {
|
||||
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
||||
is ResolvedIntent.Navigation -> {
|
||||
val openingRoomFromNotification = intent.getBooleanExtra(ROOM_OPENED_FROM_NOTIFICATION, false)
|
||||
if (openingRoomFromNotification && resolvedIntent.deeplinkData is DeeplinkData.Room) {
|
||||
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationTapOpensTimeline)
|
||||
}
|
||||
navigateTo(resolvedIntent.deeplinkData)
|
||||
}
|
||||
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
|
||||
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ import com.bumble.appyx.core.node.node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.active
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
|
|
@ -48,6 +48,10 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
|
@ -56,10 +60,13 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.Optional
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom as JoinedRoomInstance
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
|
|
@ -70,9 +77,17 @@ class RoomFlowNode(
|
|||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||
private val membershipObserver: RoomMembershipObserver,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Loading,
|
||||
initialElement = run {
|
||||
val joinedRoom = (plugins.filterIsInstance<Inputs>().first().initialElement as? RoomNavigationTarget.Root)?.joinedRoom
|
||||
if (joinedRoom != null) {
|
||||
NavTarget.JoinedRoom(joinedRoom)
|
||||
} else {
|
||||
NavTarget.Loading
|
||||
}
|
||||
},
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -82,7 +97,7 @@ class RoomFlowNode(
|
|||
val roomIdOrAlias: RoomIdOrAlias,
|
||||
val roomDescription: Optional<RoomDescription>,
|
||||
val serverNames: List<String>,
|
||||
val trigger: Optional<JoinedRoom.Trigger>,
|
||||
val trigger: Optional<JoinedRoomAnalyticsEvent.Trigger>,
|
||||
val initialElement: RoomNavigationTarget,
|
||||
) : NodeInputs
|
||||
|
||||
|
|
@ -99,15 +114,23 @@ class RoomFlowNode(
|
|||
data class JoinRoom(
|
||||
val roomId: RoomId,
|
||||
val serverNames: List<String>,
|
||||
val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger,
|
||||
val trigger: JoinedRoomAnalyticsEvent.Trigger,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||
data class JoinedRoom(
|
||||
val roomId: RoomId,
|
||||
@IgnoredOnParcel val joinedRoom: JoinedRoomInstance? = null,
|
||||
) : NavTarget {
|
||||
constructor(joinedRoom: JoinedRoomInstance) : this(joinedRoom.roomId, joinedRoom)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationTapOpensTimeline)
|
||||
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
||||
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
||||
resolveRoomId()
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +148,9 @@ class RoomFlowNode(
|
|||
}
|
||||
|
||||
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
|
||||
val roomInfoFlow = client.getRoomInfoFlow(roomId)
|
||||
val joinedRoom = (inputs.initialElement as? RoomNavigationTarget.Root)?.joinedRoom
|
||||
val roomInfoFlow = joinedRoom?.roomInfoFlow?.map { Optional.of(it) }
|
||||
?: client.getRoomInfoFlow(roomId)
|
||||
|
||||
// This observes the local membership changes for the room
|
||||
val membershipUpdateFlow = membershipObserver.updates
|
||||
|
|
@ -141,6 +166,11 @@ class RoomFlowNode(
|
|||
currentMembershipFlow.onEach { (previousMembership, membership) ->
|
||||
Timber.d("Room membership: $membership")
|
||||
if (membership == CurrentUserMembership.JOINED) {
|
||||
val currentNavTarget = backstack.active?.key?.navTarget
|
||||
if (currentNavTarget is NavTarget.JoinedRoom && currentNavTarget.roomId == roomId) {
|
||||
Timber.d("Already in JoinedRoom $roomId, do nothing")
|
||||
return@onEach
|
||||
}
|
||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||
} else {
|
||||
val leavingFromCurrentDevice =
|
||||
|
|
@ -155,7 +185,7 @@ class RoomFlowNode(
|
|||
NavTarget.JoinRoom(
|
||||
roomId = roomId,
|
||||
serverNames = serverNames,
|
||||
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
|
||||
trigger = inputs.trigger.getOrNull() ?: JoinedRoomAnalyticsEvent.Trigger.Invite,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -201,7 +231,8 @@ class RoomFlowNode(
|
|||
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
|
||||
val inputs = JoinedRoomFlowNode.Inputs(
|
||||
roomId = navTarget.roomId,
|
||||
initialElement = inputs.initialElement
|
||||
initialElement = inputs.initialElement,
|
||||
joinedRoom = navTarget.joinedRoom,
|
||||
)
|
||||
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,15 @@ package io.element.android.appnav.room
|
|||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface RoomNavigationTarget : Parcelable {
|
||||
@Parcelize
|
||||
data class Root(
|
||||
val eventId: EventId? = null,
|
||||
@IgnoredOnParcel val joinedRoom: JoinedRoom? = null,
|
||||
) : RoomNavigationTarget
|
||||
|
||||
@Parcelize
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -63,11 +64,12 @@ class JoinedRoomFlowNode(
|
|||
) {
|
||||
data class Inputs(
|
||||
val roomId: RoomId,
|
||||
val joinedRoom: JoinedRoom?,
|
||||
val initialElement: RoomNavigationTarget,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
|
||||
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId, inputs.joinedRoom)
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ import io.element.android.libraries.matrix.api.core.ThreadId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -66,6 +71,7 @@ class JoinedRoomLoadedFlowNode(
|
|||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
private val analyticsService: AnalyticsService,
|
||||
roomGraphFactory: RoomGraphFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -93,6 +99,8 @@ class JoinedRoomLoadedFlowNode(
|
|||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
|
||||
analyticsService.startLongRunningTransaction(LoadMessagesUi, parent)
|
||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
activeRoomsHolder.addRoom(inputs.room)
|
||||
|
|
@ -100,6 +108,7 @@ class JoinedRoomLoadedFlowNode(
|
|||
trackVisitedRoom()
|
||||
},
|
||||
onResume = {
|
||||
analyticsService.finishLongRunningTransaction(LoadJoinedRoomFlow)
|
||||
sessionCoroutineScope.launch {
|
||||
inputs.room.subscribeToSync()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
|
|
@ -123,6 +124,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
|||
roomGraphFactory = FakeRoomGraphFactory(),
|
||||
matrixClient = matrixClient,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Test
|
||||
|
||||
class LoadingBaseRoomStateFlowFactoryTest {
|
||||
@Test
|
||||
fun `flow should emit only Loaded when we already pass a JoinedRoom`() = runTest {
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
||||
val matrixClient = FakeMatrixClient(A_SESSION_ID)
|
||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||
flowFactory
|
||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = room)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest {
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
||||
|
|
@ -31,7 +44,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
|||
}
|
||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||
flowFactory
|
||||
.create(this, A_ROOM_ID)
|
||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
||||
|
|
@ -45,7 +58,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
|||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||
flowFactory
|
||||
.create(this, A_ROOM_ID)
|
||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -60,7 +73,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
|||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||
flowFactory
|
||||
.create(this, A_ROOM_ID)
|
||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.27")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.28")
|
||||
detektPlugins(project(":tests:detekt-rules"))
|
||||
}
|
||||
|
||||
|
|
|
|||
6
fastlane/metadata/android/en-US/changelogs/202512000.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/202512000.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Main changes in this version:
|
||||
- Improve the room security and privacy screens.
|
||||
- Better room list sorting.
|
||||
- Fixed crashes when recording long voice messages.
|
||||
- Improved the UX when opening a room from the room list.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_announcement_item4">"Присъединете се към обществени пространства"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_announcement_item1">"Visualizza gli spazi che hai creato o a cui partecipi"</string>
|
||||
<string name="screen_space_announcement_item2">"Accetta o rifiuta gli inviti agli spazi"</string>
|
||||
<string name="screen_space_announcement_item3">"Scopri tutte le stanze a cui puoi partecipare nei tuoi spazi"</string>
|
||||
<string name="screen_space_announcement_item4">"Unisciti agli spazi pubblici"</string>
|
||||
<string name="screen_space_announcement_item5">"Lascia tutti gli spazi a cui ti sei unito"</string>
|
||||
<string name="screen_space_announcement_notice">"A breve saranno disponibili le funzionalità di filtraggio, creazione e gestione degli spazi."</string>
|
||||
<string name="screen_space_announcement_subtitle">"Benvenuti alla versione beta degli Spazi! Con questa prima versione potrete:"</string>
|
||||
<string name="screen_space_announcement_title">"Ti presentiamo gli Spazi"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_announcement_item1">"Visualizar espaços que criou ou entrou"</string>
|
||||
<string name="screen_space_announcement_item2">"Aceitar ou recusar convites aos espaços"</string>
|
||||
<string name="screen_space_announcement_item3">"Descobrir quaisquer salas que você pode entrar nos espaços"</string>
|
||||
<string name="screen_space_announcement_item4">"Entrar espaços públicos"</string>
|
||||
<string name="screen_space_announcement_item5">"Sair de quaisquer espaços que entrou"</string>
|
||||
<string name="screen_space_announcement_notice">"Filtrar, criar, e gerenciar espaços virão em breve."</string>
|
||||
<string name="screen_space_announcement_subtitle">"Boas-vindas à versão beta dos Espaços! Com essa primeira versão, você pode:"</string>
|
||||
<string name="screen_space_announcement_title">"Apresentando Espaços"</string>
|
||||
</resources>
|
||||
|
|
@ -2,4 +2,10 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_announcement_item1">"查看您创建或加入的空间"</string>
|
||||
<string name="screen_space_announcement_item2">"接受或拒绝空间邀请"</string>
|
||||
<string name="screen_space_announcement_item3">"发现您可以加入空间的所有房间"</string>
|
||||
<string name="screen_space_announcement_item4">"加入公共空间"</string>
|
||||
<string name="screen_space_announcement_item5">"离开你加入的所有空间"</string>
|
||||
<string name="screen_space_announcement_notice">"筛选、创建及管理空间功能即将上线。"</string>
|
||||
<string name="screen_space_announcement_subtitle">"欢迎使用 Spaces 测试版!使用首个版本,您可以:"</string>
|
||||
<string name="screen_space_announcement_title">"Spaces 简介"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Uusi huone"</string>
|
||||
<string name="screen_create_room_add_people_title">"Kutsu ihmisiä"</string>
|
||||
<string name="screen_create_room_add_people_title">"Kutsu henkilöitä"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Huoneen luomisessa tapahtui virhe"</string>
|
||||
<string name="screen_create_room_private_option_description">"Vain kutsutut henkilöt pääsevät tähän huoneeseen. Kaikki viestit ovat päästä päähän salattuja."</string>
|
||||
<string name="screen_create_room_private_option_title">"Yksityinen huone"</string>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Você pode mudar isso a qualquer momento nas configurações da sala."</string>
|
|||
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para entrar"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
|
||||
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
interface HomeEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(
|
||||
|
|
@ -22,7 +23,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
|
|||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToRoom(roomId: RoomId)
|
||||
fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?)
|
||||
fun navigateToCreateRoom()
|
||||
fun navigateToSettings()
|
||||
fun navigateToSetUpRecovery()
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import androidx.activity.compose.LocalActivity
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
|
|
@ -41,20 +43,33 @@ import io.element.android.features.logout.api.direct.DirectLogoutView
|
|||
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.utils.DelayedVisibility
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
|
|
@ -71,6 +86,7 @@ class HomeFlowNode(
|
|||
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val leaveRoomRenderer: LeaveRoomRenderer,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : BaseFlowNode<HomeFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -150,9 +166,58 @@ class HomeFlowNode(
|
|||
return node(buildContext) { modifier ->
|
||||
val state by stateFlow.collectAsState()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
||||
val loadingJoinedRoomJob = remember { mutableStateOf<AsyncData<Job>>(AsyncData.Uninitialized) }
|
||||
if (loadingJoinedRoomJob.value.isLoading()) {
|
||||
DelayedVisibility(duration = 400.milliseconds) {
|
||||
ProgressDialog(
|
||||
onDismissRequest = {
|
||||
loadingJoinedRoomJob.value.dataOrNull()?.cancel()
|
||||
loadingJoinedRoomJob.value = AsyncData.Uninitialized
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToRoom(
|
||||
roomId: RoomId,
|
||||
) {
|
||||
if (!loadingJoinedRoomJob.value.isUninitialized()) {
|
||||
Timber.w("Already loading a room, ignoring navigateToRoom for $roomId")
|
||||
return
|
||||
}
|
||||
|
||||
val job = sessionCoroutineScope.launch {
|
||||
runCatchingExceptions {
|
||||
matrixClient.getJoinedRoom(roomId)
|
||||
}.fold(
|
||||
onSuccess = { joinedRoom ->
|
||||
if (isActive) {
|
||||
callback.navigateToRoom(roomId, joinedRoom)
|
||||
loadingJoinedRoomJob.value = AsyncData.Success(coroutineContext.job)
|
||||
// Wait a bit before resetting the state to avoid allowing to open several rooms
|
||||
delay(200.milliseconds)
|
||||
loadingJoinedRoomJob.value = AsyncData.Uninitialized
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
// If the operation wasn't cancelled, navigate without the room, using the room id
|
||||
if (it !is CancellationException) {
|
||||
callback.navigateToRoom(roomId, null)
|
||||
}
|
||||
loadingJoinedRoomJob.value = AsyncData.Failure(error = it, prevData = coroutineContext.job)
|
||||
// Wait a bit before resetting the state to avoid allowing to open several rooms
|
||||
delay(200.milliseconds)
|
||||
loadingJoinedRoomJob.value = AsyncData.Uninitialized
|
||||
}
|
||||
)
|
||||
}
|
||||
loadingJoinedRoomJob.value = AsyncData.Loading(job)
|
||||
}
|
||||
|
||||
HomeView(
|
||||
homeState = state,
|
||||
onRoomClick = callback::navigateToRoom,
|
||||
onRoomClick = ::navigateToRoom,
|
||||
onSettingsClick = callback::navigateToSettings,
|
||||
onStartChatClick = callback::navigateToCreateRoom,
|
||||
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
|
||||
|
|
@ -165,7 +230,7 @@ class HomeFlowNode(
|
|||
acceptDeclineInviteView = {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.roomListState.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = callback::navigateToRoom,
|
||||
onAcceptInviteSuccess = ::navigateToRoom,
|
||||
onDeclineInviteSuccess = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.zIndex
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.home.impl.R
|
||||
import io.element.android.features.home.impl.model.LatestEvent
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
|
||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||
|
|
@ -120,6 +121,7 @@ internal fun RoomSummaryRow(
|
|||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
latestEvent = room.latestEvent,
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
|
|
@ -136,6 +138,7 @@ internal fun RoomSummaryRow(
|
|||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
latestEvent = room.latestEvent,
|
||||
timestamp = null,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
|
|
@ -211,6 +214,7 @@ private fun RoomSummaryScaffoldRow(
|
|||
@Composable
|
||||
private fun NameAndTimestampRow(
|
||||
name: String?,
|
||||
latestEvent: LatestEvent,
|
||||
timestamp: String?,
|
||||
isHighlighted: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -219,16 +223,42 @@ private fun NameAndTimestampRow(
|
|||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(16.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||
color = ElementTheme.colors.roomListRoomName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||
color = ElementTheme.colors.roomListRoomName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Picto
|
||||
when (latestEvent) {
|
||||
is LatestEvent.Sending -> {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = CompoundIcons.Time(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
)
|
||||
}
|
||||
is LatestEvent.Error -> {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = CompoundIcons.ErrorSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
// Timestamp
|
||||
Text(
|
||||
text = timestamp ?: "",
|
||||
|
|
@ -274,21 +304,41 @@ private fun MessagePreviewAndIndicatorRow(
|
|||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(28.dp)
|
||||
) {
|
||||
val messagePreview = if (room.isTombstoned) {
|
||||
stringResource(R.string.screen_roomlist_tombstoned_room_description)
|
||||
if (room.isTombstoned) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.screen_roomlist_tombstoned_room_description),
|
||||
color = ElementTheme.colors.roomListRoomMessage,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
room.lastMessage.orEmpty()
|
||||
if (room.latestEvent is LatestEvent.Error) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(CommonStrings.common_message_failed_to_send),
|
||||
color = ElementTheme.colors.textCriticalPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val messagePreview = room.latestEvent.content()
|
||||
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString())
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = annotatedMessagePreview,
|
||||
color = ElementTheme.colors.roomListRoomMessage,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.toString())
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = annotatedMessagePreview,
|
||||
color = ElementTheme.colors.roomListRoomMessage,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
// Call and unread
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@
|
|||
package io.element.android.features.home.impl.datasource
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.home.impl.model.LatestEvent
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
|
|
@ -26,7 +28,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
@Inject
|
||||
class RoomListRoomSummaryFactory(
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||
private val roomLatestEventFormatter: RoomLatestEventFormatter,
|
||||
) {
|
||||
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
|
||||
val roomInfo = roomSummary.info
|
||||
|
|
@ -40,13 +42,11 @@ class RoomListRoomSummaryFactory(
|
|||
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
|
||||
isMarkedUnread = roomInfo.isMarkedUnread,
|
||||
timestamp = dateFormatter.format(
|
||||
timestamp = roomSummary.lastMessageTimestamp,
|
||||
timestamp = roomSummary.latestEventTimestamp,
|
||||
mode = DateFormatterMode.TimeOrDate,
|
||||
useRelative = true,
|
||||
),
|
||||
lastMessage = roomSummary.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
|
||||
}.orEmpty(),
|
||||
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.isDm),
|
||||
avatarData = avatarData,
|
||||
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
|
||||
hasRoomCall = roomInfo.hasRoomCall,
|
||||
|
|
@ -73,4 +73,28 @@ class RoomListRoomSummaryFactory(
|
|||
isSpace = roomInfo.isSpace,
|
||||
)
|
||||
}
|
||||
|
||||
private fun computeLatestEvent(latestEvent: LatestEventValue, dm: Boolean): LatestEvent {
|
||||
return when (latestEvent) {
|
||||
is LatestEventValue.None -> {
|
||||
LatestEvent.None
|
||||
}
|
||||
is LatestEventValue.Local -> {
|
||||
if (latestEvent.isSending) {
|
||||
val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty()
|
||||
LatestEvent.Sending(
|
||||
content = content,
|
||||
)
|
||||
} else {
|
||||
LatestEvent.Error
|
||||
}
|
||||
}
|
||||
is LatestEventValue.Remote -> {
|
||||
val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty()
|
||||
LatestEvent.Synced(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface LatestEvent {
|
||||
data object None : LatestEvent
|
||||
|
||||
data class Synced(
|
||||
val content: CharSequence?,
|
||||
) : LatestEvent
|
||||
|
||||
data class Sending(
|
||||
val content: CharSequence?,
|
||||
) : LatestEvent
|
||||
|
||||
data object Error : LatestEvent
|
||||
|
||||
fun content(): CharSequence? {
|
||||
return when (this) {
|
||||
is None -> null
|
||||
is Synced -> content
|
||||
is Sending -> content
|
||||
is Error -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ data class RoomListRoomSummary(
|
|||
val numberOfUnreadNotifications: Long,
|
||||
val isMarkedUnread: Boolean,
|
||||
val timestamp: String?,
|
||||
val lastMessage: CharSequence?,
|
||||
val latestEvent: LatestEvent,
|
||||
val avatarData: AvatarData,
|
||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||
val hasRoomCall: Boolean,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
|
||||
aRoomListRoomSummary(),
|
||||
aRoomListRoomSummary(name = null),
|
||||
aRoomListRoomSummary(lastMessage = null),
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.None),
|
||||
aRoomListRoomSummary(
|
||||
name = "A very long room name that should be truncated",
|
||||
lastMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
|
||||
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
latestEvent = LatestEvent.Synced(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
|
||||
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
|
||||
),
|
||||
timestamp = "yesterday",
|
||||
numberOfUnreadMessages = 1,
|
||||
),
|
||||
|
|
@ -44,7 +46,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
listOf(
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "No activity" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Synced("No activity" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 0,
|
||||
|
|
@ -52,7 +54,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New messages" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Synced("New messages" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 1,
|
||||
numberOfUnreadMentions = 0,
|
||||
|
|
@ -60,7 +62,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New messages, mentions" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Synced("New messages, mentions" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 1,
|
||||
numberOfUnreadMentions = 1,
|
||||
|
|
@ -68,7 +70,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New mentions" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Synced("New mentions" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 1,
|
||||
|
|
@ -127,6 +129,10 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
|||
isTombstoned = true,
|
||||
)
|
||||
),
|
||||
listOf(
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
|
||||
)
|
||||
).flatten()
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +154,8 @@ internal fun aRoomListRoomSummary(
|
|||
numberOfUnreadMentions: Long = 0,
|
||||
numberOfUnreadNotifications: Long = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
lastMessage: String? = "Last message",
|
||||
timestamp: String? = lastMessage?.let { "88:88" },
|
||||
latestEvent: LatestEvent = LatestEvent.Synced("Last message"),
|
||||
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||
|
|
@ -171,7 +177,7 @@ internal fun aRoomListRoomSummary(
|
|||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
timestamp = timestamp,
|
||||
lastMessage = lastMessage,
|
||||
latestEvent = latestEvent,
|
||||
avatarData = avatarData,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import io.element.android.libraries.preferences.api.store.SessionPreferencesStor
|
|||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
|
@ -86,6 +87,7 @@ class RoomListPresenter(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService = client.encryptionService
|
||||
|
||||
|
|
@ -236,6 +238,8 @@ class RoomListPresenter(
|
|||
)
|
||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||
else -> {
|
||||
coldStartWatcher.onRoomListVisible()
|
||||
|
||||
RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ package io.element.android.features.home.impl.roomlist
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.features.home.impl.model.LatestEvent
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.features.home.impl.model.aRoomListRoomSummary
|
||||
|
|
@ -88,7 +89,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
|||
name = "Room",
|
||||
numberOfUnreadMessages = 1,
|
||||
timestamp = "14:18",
|
||||
lastMessage = "A very very very very long message which suites on two lines",
|
||||
latestEvent = LatestEvent.Synced("A very very very very long message which suites on two lines"),
|
||||
avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId:domain",
|
||||
),
|
||||
|
|
@ -96,7 +97,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
|||
name = "Room#2",
|
||||
numberOfUnreadMessages = 0,
|
||||
timestamp = "14:16",
|
||||
lastMessage = "A short message",
|
||||
latestEvent = LatestEvent.Synced("A short message"),
|
||||
avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId2:domain",
|
||||
),
|
||||
|
|
@ -119,7 +120,7 @@ internal fun generateRoomListRoomSummaryList(
|
|||
name = "Room#$index",
|
||||
numberOfUnreadMessages = 0,
|
||||
timestamp = "14:16",
|
||||
lastMessage = "A message",
|
||||
latestEvent = LatestEvent.Synced("A message"),
|
||||
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId$index:domain",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="banner_set_up_recovery_content">"بازگردانی تاریخچهٔ پیامها و هویت رمزنگاشتهتان با کلید بازیابی در صورت از دست دادن همهٔ افزارههای موجودتان."</string>
|
||||
<string name="banner_set_up_recovery_submit">"برپایی بازیابی"</string>
|
||||
<string name="banner_set_up_recovery_title">"برپایی بازیابی"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"کلید بازیابی خود را تأیید کنید تا دسترسی به حافظه کلیدها و تاریخچه پیامهایتان حفظ شود ."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"ورود کلید بازیابیتان"</string>
|
||||
<string name="confirm_recovery_key_banner_title">"ذخیرهساز کلیدتان از همگام بودن در آمده"</string>
|
||||
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
<string name="banner_battery_optimization_content_android">"Disabilita l\'ottimizzazione della batteria per questa app, per assicurarti che tutte le notifiche vengano ricevute."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Disabilita l\'ottimizzazione"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Le notifiche non arrivano?"</string>
|
||||
<string name="banner_new_sound_message">"Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso."</string>
|
||||
<string name="banner_new_sound_title">"Abbiamo rinnovato i tuoi suoni"</string>
|
||||
<string name="banner_set_up_recovery_content">"Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Configura il recupero"</string>
|
||||
<string name="banner_set_up_recovery_title">"Configura il ripristino"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
<string name="banner_battery_optimization_content_android">"Desative a otimização de bateria para este app, para que tenha certeza que todas as notificações sejam recebidas."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Desativar otimização"</string>
|
||||
<string name="banner_battery_optimization_title_android">"As notificações não chegam?"</string>
|
||||
<string name="banner_new_sound_message">"O seu ping de notificação foi atualizado—mais suave, mais rápido, e menos disruptivo."</string>
|
||||
<string name="banner_new_sound_title">"Recarregamos seus sons"</string>
|
||||
<string name="banner_set_up_recovery_content">"Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação caso você perda todos os dispositivos existentes."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Configurar a recuperação"</string>
|
||||
<string name="banner_set_up_recovery_title">"Configure a recuperação para proteger sua conta"</string>
|
||||
|
|
@ -33,6 +35,7 @@ Por enquanto, você pode desmarcar os filtros para ver suas outras conversas"</s
|
|||
<string name="screen_roomlist_filter_invites">"Convites"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Você não tem nenhum convite pendente."</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Baixa prioridade"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Você ainda não tem nenhuma conversa de baixa prioridade"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Você pode desmarcar filtros para ver suas outras conversas"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Você não tem conversas para esta seleção"</string>
|
||||
<string name="screen_roomlist_filter_people">"Pessoas"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="full_screen_intent_banner_message">"Muhim qoʻngʻiroqlarni oʻtkazib yubormasligingiz uchun telefoningiz qulflangan holatida toʻliq ekranli bildirishnomalarni ko‘rsatishga ruxsat beradigan qilib sozlamalaringizni oʻzgartiring."</string>
|
||||
<string name="full_screen_intent_banner_title">"Qoʻngʻiroq tajribangizni yaxshilang"</string>
|
||||
<string name="screen_home_tab_chats">"Suhbatlar"</string>
|
||||
<string name="screen_home_tab_spaces">"Bo‘shliqlar"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Taklifni rad etish"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?"</string>
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
<string name="screen_migration_message">"Bu bir martalik jarayon, kutganingiz uchun rahmat."</string>
|
||||
<string name="screen_migration_title">"Hisobingiz sozlanmoqda."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Yangi suhbat yoki xona yarating"</string>
|
||||
<string name="screen_roomlist_clear_filters">"Filtrlarni tozalash"</string>
|
||||
<string name="screen_roomlist_empty_message">"Kimgadir xabar yuborishdan boshlang."</string>
|
||||
<string name="screen_roomlist_empty_title">"Hozircha chatlar yo‘q."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Sevimlilar"</string>
|
||||
|
|
@ -31,6 +33,7 @@ Hozircha, boshqa suhbatlaringizni ko‘rish uchun filtrlarni bekor qilishingiz m
|
|||
<string name="screen_roomlist_filter_invites">"Takliflar"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Sizda hech qanday kutilayotgan takliflar yoʻq."</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Past darajali"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sizda hali past ustuvor chatlar yoʻq"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Boshqa suhbatlaringizni koʻrish uchun filtrlarni bekor qilishingiz mumkin"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Sizda bu tanlov uchun chatlar yo‘q"</string>
|
||||
<string name="screen_roomlist_filter_people">"Odamlar"</string>
|
||||
|
|
@ -44,6 +47,7 @@ Sizda oʻqilmagan xabarlar yoʻq!"</string>
|
|||
<string name="screen_roomlist_main_space_title">"Suhbatlar"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Oʻqilgan deb belgilash"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Oʻqilmagan deb belgilash"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Bu xona yangilandi"</string>
|
||||
<string name="session_verification_banner_message">"Siz yangi qurilmadan foydalanayotganga o‘xshaysiz. Shifrlangan xabarlaringizga kirish uchun boshqa qurilma bilan tasdiqlang."</string>
|
||||
<string name="session_verification_banner_title">"Siz ekanligingizni tasdiqlang"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
<string name="banner_battery_optimization_content_android">"请关闭本应用的电池优化设置,确保不错过任何消息通知。"</string>
|
||||
<string name="banner_battery_optimization_submit_android">"禁用优化"</string>
|
||||
<string name="banner_battery_optimization_title_android">"通知未送达?"</string>
|
||||
<string name="banner_new_sound_message">"您的通知提示音已升级 - 更清晰、更快速、干扰更少。"</string>
|
||||
<string name="banner_new_sound_title">"我们已更新您的声音"</string>
|
||||
<string name="banner_set_up_recovery_content">"生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"</string>
|
||||
<string name="banner_set_up_recovery_submit">"设置恢复"</string>
|
||||
<string name="banner_set_up_recovery_title">"设置恢复"</string>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,19 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultHomeEntryPointTest {
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultHomeEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
HomeFlowNode(
|
||||
|
|
@ -39,10 +41,11 @@ class DefaultHomeEntryPointTest {
|
|||
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
|
||||
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
|
||||
leaveRoomRenderer = { _, _, _ -> lambdaError() },
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
val callback = object : HomeEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId) = lambdaError()
|
||||
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError()
|
||||
override fun navigateToCreateRoom() = lambdaError()
|
||||
override fun navigateToSettings() = lambdaError()
|
||||
override fun navigateToSetUpRecovery() = lambdaError()
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ package io.element.android.features.home.impl.datasource
|
|||
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||
|
||||
fun aRoomListRoomSummaryFactory(
|
||||
dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
|
||||
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
) = RoomListRoomSummaryFactory(
|
||||
dateFormatter = dateFormatter,
|
||||
roomLastMessageFormatter = roomLastMessageFormatter
|
||||
roomLatestEventFormatter = roomLatestEventFormatter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ internal fun createRoomListRoomSummary(
|
|||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
timestamp = timestamp,
|
||||
lastMessage = "",
|
||||
latestEvent = LatestEvent.Synced(""),
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||
displayType = displayType,
|
||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
|||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
|
|
@ -638,7 +639,7 @@ class RoomListPresenterTest {
|
|||
client: MatrixClient = FakeMatrixClient(),
|
||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||
|
|
@ -655,7 +656,7 @@ class RoomListPresenterTest {
|
|||
roomListService = client.roomListService,
|
||||
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
dateFormatter = dateFormatter,
|
||||
roomLastMessageFormatter = roomLastMessageFormatter,
|
||||
roomLatestEventFormatter = roomLatestEventFormatter,
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
notificationSettingsService = client.notificationSettingsService,
|
||||
|
|
@ -673,5 +674,6 @@ class RoomListPresenterTest {
|
|||
appPreferencesStore = appPreferencesStore,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
announcementService = announcementService,
|
||||
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ class RoomListViewTest {
|
|||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
rule.onNodeWithText(room0.lastMessage!!.toString()).performClick()
|
||||
rule.onNodeWithText(room0.latestEvent.content().toString()).performClick()
|
||||
}
|
||||
|
||||
eventsRecorder.assertEmpty()
|
||||
|
|
@ -192,7 +192,7 @@ class RoomListViewTest {
|
|||
)
|
||||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
rule.onNodeWithText(room0.lastMessage!!.toString())
|
||||
rule.onNodeWithText(room0.latestEvent.content().toString())
|
||||
.performClick()
|
||||
.performClick()
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ class RoomListViewTest {
|
|||
// Remove automatic initial events
|
||||
eventsRecorder.clear()
|
||||
|
||||
rule.onNodeWithText(room0.lastMessage!!.toString()).performTouchInput { longClick() }
|
||||
rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() }
|
||||
eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
|
|
@ -126,7 +126,7 @@ fun TestScope.createRoomListSearchPresenter(
|
|||
roomListService = roomListService,
|
||||
roomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||
),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_decline_and_block_block_user_option_description">"Siz bu foydalanuvchidan hech qanday xabar yoki xonaga taklif ko‘rmaysiz"</string>
|
||||
<string name="screen_decline_and_block_block_user_option_title">"Foydalanuvchini bloklash"</string>
|
||||
<string name="screen_decline_and_block_report_user_option_description">"Bu xona haqida hisobingiz provayderiga xabar bering."</string>
|
||||
<string name="screen_decline_and_block_report_user_reason_placeholder">"Xabar berish sababini tushuntiring…"</string>
|
||||
<string name="screen_decline_and_block_title">"Rad etish va bloklash"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Taklifni rad etish"</string>
|
||||
|
|
@ -8,5 +11,8 @@
|
|||
<string name="screen_invites_decline_direct_chat_title">"Chatni rad etish"</string>
|
||||
<string name="screen_invites_empty_list">"Takliflar yo\'q"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s ) sizni taklif qildi"</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ha, rad etish va bloklash"</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_message">"Ushbu xonaga qo‘shilish taklifini rad etishga ishonchingiz komilmi? Bu %1$sning siz bilan bog‘lanishiga yoki sizni xonalarga taklif qilishiga ham to‘sqinlik qiladi."</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_title">"Taklifni rad etish va bloklash"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Rad etish va bloklash"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_ban_by_message">"Sei stato bandito da questa stanza da %1$s."</string>
|
||||
<string name="screen_join_room_ban_message">"Sei stato bandito da questa stanza"</string>
|
||||
<string name="screen_join_room_ban_by_message">"Sei stato bannato da %1$s ."</string>
|
||||
<string name="screen_join_room_ban_message">"Sei stato bannato"</string>
|
||||
<string name="screen_join_room_ban_reason">"Motivo: %1$s"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancella richiesta"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sì, annulla"</string>
|
||||
|
|
@ -11,10 +11,11 @@
|
|||
<string name="screen_join_room_decline_and_block_alert_message">"Sei sicuro di voler rifiutare l\'invito a entrare in questa stanza? Ciò impedirà a %1$s di contattarti o invitarti nuovamente in una stanza."</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_title">"Rifiuta invito e blocca"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Rifiuta e blocca"</string>
|
||||
<string name="screen_join_room_fail_message">"L\'accesso alla stanza non è riuscito."</string>
|
||||
<string name="screen_join_room_fail_reason">"Questa stanza è solo su invito o potrebbero esserci delle restrizioni all\'accesso al livello dello spazio."</string>
|
||||
<string name="screen_join_room_forget_action">"Dimentica questa stanza"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Hai bisogno di un invito per entrare in questa stanza"</string>
|
||||
<string name="screen_join_room_fail_message">"Partecipazione non riuscita"</string>
|
||||
<string name="screen_join_room_fail_reason">"Devi essere invitato per partecipare o potrebbero esserci delle restrizioni di accesso."</string>
|
||||
<string name="screen_join_room_forget_action">"Dimentica"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Per partecipare è necessario un invito"</string>
|
||||
<string name="screen_join_room_invited_by">"Invitato da"</string>
|
||||
<string name="screen_join_room_join_action">"Entra"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Potrebbe essere necessario essere invitati o essere membro di uno spazio per partecipare."</string>
|
||||
<string name="screen_join_room_knock_action">"Bussa per partecipare"</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_ban_by_message">"Você foi banido desta sala por %1$s."</string>
|
||||
<string name="screen_join_room_ban_message">"Você foi banido desta sala"</string>
|
||||
<string name="screen_join_room_ban_by_message">"Você foi banido por %1$s."</string>
|
||||
<string name="screen_join_room_ban_message">"Você foi banido"</string>
|
||||
<string name="screen_join_room_ban_reason">"Motivo: %1$s."</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancelar"</string>
|
||||
|
|
@ -11,10 +11,11 @@
|
|||
<string name="screen_join_room_decline_and_block_alert_message">"Tem certeza de que quer recusar o convite para entrar nesta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para salas."</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite e bloquear"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
|
||||
<string name="screen_join_room_fail_message">"A entrada na sala falhou."</string>
|
||||
<string name="screen_join_room_fail_reason">"Esta sala é apenas para convidados ou pode haver restrições de acesso a nível do espaço."</string>
|
||||
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Você precisa de um convite para entrar nesta sala"</string>
|
||||
<string name="screen_join_room_fail_message">"Falha ao entrar"</string>
|
||||
<string name="screen_join_room_fail_reason">"Você precisa ser convidado ou pode haver restrições ao acesso."</string>
|
||||
<string name="screen_join_room_forget_action">"Esquecer"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Você precisa de um convite para entrar"</string>
|
||||
<string name="screen_join_room_invited_by">"Convidado por"</string>
|
||||
<string name="screen_join_room_join_action">"Entrar"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Talvez você precise ser convidado ou ser membro de um espaço para participar."</string>
|
||||
<string name="screen_join_room_knock_action">"Enviar solicitação para entrar"</string>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_ban_by_message">"Siz %1$s tomonidan ushbu xonadan ban qilingansiz."</string>
|
||||
<string name="screen_join_room_ban_message">"Siz bu xonadan chetlashtirilgansiz"</string>
|
||||
<string name="screen_join_room_ban_reason">"Sababi: %1$s ."</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"So‘rovni bekor qilish"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ha, bekor qiling"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Bu xonaga qo‘shilish so‘rovingizni bekor qilishni xohlayotganingizga ishonchingiz komilmi?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Qo‘shilish so‘rovini bekor qilish"</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ha, rad etish va bloklash"</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_message">"Ushbu xonaga qo‘shilish taklifini rad etishga ishonchingiz komilmi? Bu %1$sning siz bilan bog‘lanishiga yoki sizni xonalarga taklif qilishiga ham to‘sqinlik qiladi."</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_title">"Taklifni rad etish va bloklash"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Rad etish va bloklash"</string>
|
||||
<string name="screen_join_room_fail_message">"Xonaga qo‘shilish amalga oshmadi"</string>
|
||||
<string name="screen_join_room_fail_reason">"Bu xona faqat taklif etilganlar uchun yoki bu maydonga kirish huquqi cheklangan bo‘lishi mumkin."</string>
|
||||
<string name="screen_join_room_forget_action">"Bu xonani esdan chiqarish"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Bu xonaga kirish uchun taklifnoma kerak"</string>
|
||||
<string name="screen_join_room_join_action">"Qo\'shilish"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Qo‘shilish uchun sizga taklif kerak yoki siz maydonga a’zo bo‘lishingiz kerak."</string>
|
||||
<string name="screen_join_room_knock_action">"Qoʻshilish soʻrovini yuborish"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Ruxsat etilgan belgilar: %1$d / %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Xabar (ixtiyoriy)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Agar so‘rovingiz qabul qilinsa, xonaga qo‘shilish taklifini olasiz."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Qo‘shilish so‘rovi yuborildi"</string>
|
||||
<string name="screen_join_room_loading_alert_message">"Xona ko‘rinishini namoyish eta olmadik. Bu tarmoq yoki server muammolari tufayli yuz bergan bo‘lishi mumkin."</string>
|
||||
<string name="screen_join_room_loading_alert_title">"Biz bu xonani oldindan ko‘rishni ko‘rsata olmadik "</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s hali maydon xizmatini qoʻllab-quvvatlamaydi. maydonga veb-sayt orqali kirishingiz mumkin."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Maydonlar hali qoʻllab-quvvatlanmaydi"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Quyidagi tugmani bosing va xona administratoriga xabar beriladi. Ruxsat berilgandan soʻng suhbatga qoʻshilishingiz mumkin boʻladi."</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_ban_by_message">"你被 %1$s 从此房间封禁。"</string>
|
||||
<string name="screen_join_room_ban_message">"你已被此房间封禁"</string>
|
||||
<string name="screen_join_room_ban_by_message">"您已被禁止访问%1$s。"</string>
|
||||
<string name="screen_join_room_ban_message">"你已被禁止访问"</string>
|
||||
<string name="screen_join_room_ban_reason">"理由:%1$s。"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"取消请求"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"是的,取消"</string>
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
<string name="screen_join_room_decline_and_block_alert_message">"您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"</string>
|
||||
<string name="screen_join_room_decline_and_block_alert_title">"拒绝邀请并屏蔽"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"拒绝并屏蔽"</string>
|
||||
<string name="screen_join_room_fail_message">"加入房间失败。"</string>
|
||||
<string name="screen_join_room_fail_reason">"要么此房间仅限受邀者,要么可能在空间层级有加入限制。"</string>
|
||||
<string name="screen_join_room_forget_action">"忘记这个房间"</string>
|
||||
<string name="screen_join_room_invite_required_message">"你需要邀请才能加入这个房间"</string>
|
||||
<string name="screen_join_room_fail_message">"加入失败"</string>
|
||||
<string name="screen_join_room_fail_reason">"您需要被邀请加入,否则可能会受到访问限制。"</string>
|
||||
<string name="screen_join_room_forget_action">"忘记"</string>
|
||||
<string name="screen_join_room_invite_required_message">"您需要邀请才能加入"</string>
|
||||
<string name="screen_join_room_invited_by">"受邀于"</string>
|
||||
<string name="screen_join_room_join_action">"加入"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"您可能需要受到邀请或成为某个空间的成员才能加入。"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Ha, hammasini qabul qiling"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Barcha qo‘shilish so‘rovlarini qabul qilishga ishonchingiz komilmi?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Barcha so‘rovlarni qabul qilish"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Hammasini qabul qiling"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Biz barcha so‘rovlarni qabul qila olmadik. Qayta urinib koʻrmoqchimisiz?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Barcha so‘rovlar qabul qilinmadi"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_loading_title">"Qo‘shilish so‘rovi qabul qilinmoqda"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_description">"Biz bu so‘rovni qabul qila olmadik. Yana bir bor urinib ko‘rishni xohlaysizmi?"</string>
|
||||
<string name="screen_knock_requests_list_accept_failed_alert_title">"So‘rovni qabul qilib bo‘lmadi"</string>
|
||||
<string name="screen_knock_requests_list_accept_loading_title">"Qo‘shilish so‘rovi qabul qilinmoqda"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ha, rad eting va taqiqlang"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Siz %1$sʼni rad etib, taqiqlashni xohlayotganingizga ishonchingiz komilmi? Bu foydalanuvchi ushbu xonaga qayta kirish uchun ruxsat so‘ray olmaydi."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Rad etish va kirishni taqiqlash"</string>
|
||||
<string name="screen_knock_requests_list_ban_loading_title">"Kirishni rad etish va taqiqlash"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ha, rad etish"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"%1$sning bu xonaga qo‘shilish so‘rovini rad etasizmi?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Kirishni rad etish"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Rad etish va taqiqlash"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_description">"Biz bu iltimosni rad etolmasdik. Yana bir bor urinib ko‘rishni xohlaysizmi?"</string>
|
||||
<string name="screen_knock_requests_list_decline_failed_alert_title">"So‘rovni rad etib bo‘lmadi"</string>
|
||||
<string name="screen_knock_requests_list_decline_loading_title">"Qo‘shilish so‘rovi rad etilayapti"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"Kimdir xonaga qo‘shilishni so‘raganda, uning iltimosini shu yerda ko‘rishingiz mumkin."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"Qo‘shilish so‘rovi kutilmayapti"</string>
|
||||
<string name="screen_knock_requests_list_initial_loading_title">"Qo‘shilish uchun so‘rovlar yuklanmoqda…"</string>
|
||||
<string name="screen_knock_requests_list_title">"Qo‘shilish uchun so‘rovlar"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
<item quantity="one">"%1$s + %2$d kishi bu xonaga qo‘shilmoqchi"</item>
|
||||
<item quantity="other">"%1$s + %2$d kishi bu xonaga qo‘shilmoqchi"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Hammasini ko\'rish"</string>
|
||||
<string name="screen_room_single_knock_request_accept_button_title">"Qabul qiling"</string>
|
||||
<string name="screen_room_single_knock_request_title">"%1$s bu xonaga qo‘shilmoqchi"</string>
|
||||
<string name="screen_room_single_knock_request_view_button_title">"Ko\'rish"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Bu suhbatni tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu suhbat hammaga ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Siz bu yerda yagona odamsiz. Agar siz tark etsangiz, kelajakda hech kim qo\'shila olmaydi, jumladan siz ham."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu xona ochiq emas va siz taklifsiz qayta qo‘shila olmaysiz."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Egalarni tanlang"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Siz bu xonaning yagona egasisiz. Xonadan chiqishdan oldin egalikni boshqaga topshirishingiz kerak."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Egalikni topshirish"</string>
|
||||
<string name="leave_room_alert_subtitle">"Xonani tark etmoqchi ekanligingizga ishonchingiz komilmi?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@
|
|||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"پینها مطابق نیستند"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"دارید خارج میشوید"</string>
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="one">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
|
||||
<item quantity="other">"شما %1$d تلاش برای باز کردن قفل دارید"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
|
||||
<item quantity="other">"پین اشتباه است. شما %1$d شانس دیگر دارید"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"استفاده از زیستسنجی"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"استفاده از پین"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="screen_app_lock_biometric_authentication">"biometrik autentifikatsiya"</string>
|
||||
<string name="screen_app_lock_biometric_unlock">"biometrik qulf ochish"</string>
|
||||
<string name="screen_app_lock_biometric_unlock_title_android">"Biometrik bilan qulfni oching"</string>
|
||||
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biometrikni tasdiqlang"</string>
|
||||
<string name="screen_app_lock_forgot_pin">"PIN kodni unutdingizmi?"</string>
|
||||
<string name="screen_app_lock_settings_change_pin">"PIN kodni o\'zgartirish"</string>
|
||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrik qulfni ochishga ruxsat bering"</string>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ interface LoginEntryPoint : FeatureEntryPoint {
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToBugReport()
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
fun createNode(
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ class LoginFlowNode(
|
|||
override fun navigateToLoginPassword() {
|
||||
backstack.push(NavTarget.LoginPassword)
|
||||
}
|
||||
|
||||
override fun onDone() {
|
||||
callback.onDone()
|
||||
}
|
||||
}
|
||||
val params = inputs<Params>()
|
||||
val inputs = OnBoardingNode.Params(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class OnBoardingNode(
|
|||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
|
|
@ -71,7 +72,7 @@ class OnBoardingNode(
|
|||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
onBackClick = ::navigateUp,
|
||||
onBackClick = callback::onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,17 @@
|
|||
<string name="screen_change_account_provider_other">"Boshqa"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Shaxsiy serveringiz yoki ishchi hisob qaydnomangiz kabi boshqa hisob provayderidan foydalaning."</string>
|
||||
<string name="screen_change_account_provider_title">"Hisob provayderini o\'zgartiring"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"%1$s da Element Pro ilovasi talab qilinadi. Iltimos, do‘kondan yuklab oling."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Element Pro talab qilinadi"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Bu uy serveriga kira olmadik. Iltimos, uy serverining URL manzilini to\'ri kiritganingizni tekshiring. Agar URL toʻgʻri boʻlsa, qoʻshimcha yordam olish uchun uy serveri administratoriga murojaat qiling."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">".well-known faylidagi muammo tufayli server mavjud emas: %1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Tanlangan hisob provayderi siljitish sinxronizatsiyasini qo‘llab-quvvatlamaydi. %1$s ishlatish uchun serverni yangilash zarur."</string>
|
||||
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s uchun %2$s bilan ulanishga ruxsat berilmagan."</string>
|
||||
<string name="screen_change_server_error_unauthorized_homeserver_content">"Bu ilova quyidagilarga ruxsat berish uchun sozlangan: %1$s ."</string>
|
||||
<string name="screen_change_server_error_unauthorized_homeserver_title">"Hisob provayderi %1$s ga ruxsat berilmagan."</string>
|
||||
<string name="screen_change_server_form_header">"Uy serverining URL manzili"</string>
|
||||
<string name="screen_change_server_form_notice">"Domen manzilini kiriting."</string>
|
||||
<string name="screen_change_server_subtitle">"Serveringizning manzili nima?"</string>
|
||||
<string name="screen_change_server_title">"Serveringizni tanlang"</string>
|
||||
<string name="screen_create_account_title">"Hisob yaratish"</string>
|
||||
|
|
@ -28,6 +36,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
|
||||
<string name="screen_login_title">"Qaytib kelganingizdan xursandmiz!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Kirish%1$s"</string>
|
||||
<string name="screen_onboarding_app_version">"%1$s versiya"</string>
|
||||
<string name="screen_onboarding_sign_in_manually">"Qo\'lda tizimga kiring"</string>
|
||||
<string name="screen_onboarding_sign_in_to">"Kirish%1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"QR kod bilan tizimga kiring"</string>
|
||||
|
|
@ -83,5 +92,6 @@ Oddiy usulda kiring yoki boshqa qurilma bilan QR kodni skanerlang."</string>
|
|||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
|
||||
<string name="screen_server_confirmation_message_register">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Siz tizimga kirmoqchisiz%1$s"</string>
|
||||
<string name="screen_server_confirmation_title_picker_mode">"Hisob provayderini tanlang"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Hisob yaratmoqchisiz%1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class DefaultLoginEntryPointTest {
|
|||
}
|
||||
val callback = object : LoginEntryPoint.Callback {
|
||||
override fun navigateToBugReport() = lambdaError()
|
||||
override fun onDone() = lambdaError()
|
||||
}
|
||||
val params = LoginEntryPoint.Params(
|
||||
accountProvider = "ac",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="screen_signout_in_progress_dialog_content">"خارج شدن…"</string>
|
||||
<string name="screen_signout_key_backup_disabled_subtitle">"دارید از واپسین نشستتان خارج میشوید. اگر اکنون خارج شوید پیامهای رمزنگاشتهتان را از دست خواهید داد."</string>
|
||||
<string name="screen_signout_key_backup_disabled_title">"پشتیبان را خاموش کردهاید"</string>
|
||||
<string name="screen_signout_key_backup_offline_subtitle">"در هنگامی که آفلاین شدید، کلیدهای شما هنوز در حال پشتیبانگیری بودند. دوباره متصل شوید ، تا قبل از خروج از کلیدهایتان نسخه پشتیبان گرفته شود."</string>
|
||||
<string name="screen_signout_key_backup_offline_title">"کلیدهایتان هنوز در حال پشتیبان گیریند"</string>
|
||||
<string name="screen_signout_key_backup_ongoing_subtitle">"لطفاً پیش از خروج منتظر پایانش شوید."</string>
|
||||
<string name="screen_signout_key_backup_ongoing_title">"کلیدهایتان هنوز در حال پشتیبان گیریند"</string>
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@ import androidx.lifecycle.Lifecycle
|
|||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
|
||||
sealed interface VoiceMessageComposerEvents {
|
||||
sealed interface VoiceMessageComposerEvent {
|
||||
data class RecorderEvent(
|
||||
val recorderEvent: VoiceMessageRecorderEvent
|
||||
) : VoiceMessageComposerEvents
|
||||
) : VoiceMessageComposerEvent
|
||||
data class PlayerEvent(
|
||||
val playerEvent: VoiceMessagePlayerEvent,
|
||||
) : VoiceMessageComposerEvents
|
||||
data object SendVoiceMessage : VoiceMessageComposerEvents
|
||||
data object DeleteVoiceMessage : VoiceMessageComposerEvents
|
||||
data object AcceptPermissionRationale : VoiceMessageComposerEvents
|
||||
data object DismissPermissionsRationale : VoiceMessageComposerEvents
|
||||
data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvents
|
||||
data object DismissSendFailureDialog : VoiceMessageComposerEvents
|
||||
) : VoiceMessageComposerEvent
|
||||
data object SendVoiceMessage : VoiceMessageComposerEvent
|
||||
data object DeleteVoiceMessage : VoiceMessageComposerEvent
|
||||
data object AcceptPermissionRationale : VoiceMessageComposerEvent
|
||||
data object DismissPermissionsRationale : VoiceMessageComposerEvent
|
||||
data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvent
|
||||
data object DismissSendFailureDialog : VoiceMessageComposerEvent
|
||||
}
|
||||
|
|
@ -17,5 +17,5 @@ data class VoiceMessageComposerState(
|
|||
val showPermissionRationaleDialog: Boolean,
|
||||
val showSendFailureDialog: Boolean,
|
||||
val keepScreenOn: Boolean,
|
||||
val eventSink: (VoiceMessageComposerEvents) -> Unit,
|
||||
val eventSink: (VoiceMessageComposerEvent) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -136,6 +138,9 @@ class MessagesNode(
|
|||
onCreate = {
|
||||
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
|
||||
},
|
||||
onResume = {
|
||||
analyticsService.finishLongRunningTransaction(LoadMessagesUi)
|
||||
},
|
||||
onDestroy = {
|
||||
mediaPlayer.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -120,7 +120,7 @@ fun MessagesView(
|
|||
knockRequestsBannerView: @Composable () -> Unit,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.LifecycleEvent(event))
|
||||
}
|
||||
|
||||
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
|
||||
|
|
@ -399,17 +399,17 @@ private fun MessagesViewContent(
|
|||
if (state.voiceMessageComposerState.showPermissionRationaleDialog) {
|
||||
VoiceMessagePermissionRationaleDialog(
|
||||
onContinue = {
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale)
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
|
||||
},
|
||||
onDismiss = {
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale)
|
||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
|
||||
},
|
||||
appName = state.appName
|
||||
)
|
||||
}
|
||||
if (state.voiceMessageComposerState.showSendFailureDialog) {
|
||||
VoiceMessageSendingFailedDialog(
|
||||
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) },
|
||||
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
|
||||
|
|
@ -78,19 +78,19 @@ internal fun MessageComposerView(
|
|||
}
|
||||
|
||||
val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent ->
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecorderEvent(press))
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvent.RecorderEvent(press))
|
||||
}
|
||||
|
||||
val onSendVoiceMessage = {
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
}
|
||||
|
||||
val onDeleteVoiceMessage = {
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||
}
|
||||
|
||||
val onVoicePlayerEvent = { event: VoiceMessagePlayerEvent ->
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.PlayerEvent(event))
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvent.PlayerEvent(event))
|
||||
}
|
||||
|
||||
TextComposer(
|
||||
|
|
|
|||
|
|
@ -55,11 +55,17 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -86,6 +92,7 @@ class TimelinePresenter(
|
|||
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
|
||||
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<TimelineState> {
|
||||
private val tag = "TimelinePresenter"
|
||||
@AssistedFactory
|
||||
|
|
@ -108,6 +115,11 @@ class TimelinePresenter(
|
|||
|
||||
@Composable
|
||||
override fun present(): TimelineState {
|
||||
LaunchedEffect(Unit) {
|
||||
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
|
||||
analyticsService.startLongRunningTransaction(DisplayFirstTimelineItems, parent)
|
||||
}
|
||||
|
||||
val localScope = rememberCoroutineScope()
|
||||
|
||||
val timelineMode = remember { timelineController.mainTimelineMode() }
|
||||
|
|
@ -195,6 +207,9 @@ class TimelinePresenter(
|
|||
focusOnEvent(event.eventId, focusRequestState)
|
||||
}.start()
|
||||
is TimelineEvents.OnFocusEventRender -> {
|
||||
// If there was a pending 'notification tap opens timeline' transaction, finish it now we're focused in the required event
|
||||
analyticsService.finishLongRunningTransaction(NotificationTapOpensTimeline)
|
||||
|
||||
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
||||
}
|
||||
is TimelineEvents.ClearFocusRequestState -> {
|
||||
|
|
@ -227,17 +242,27 @@ class TimelinePresenter(
|
|||
.onEach { newTimelineItems ->
|
||||
timelineItemIndexer.process(newTimelineItems)
|
||||
timelineItems = newTimelineItems
|
||||
|
||||
analyticsService.run {
|
||||
finishLongRunningTransaction(DisplayFirstTimelineItems)
|
||||
finishLongRunningTransaction(OpenRoom)
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
|
||||
val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems)
|
||||
val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items")
|
||||
transaction?.setData("items", items.count())
|
||||
timelineItemsFactory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = membersState.roomMembers().orEmpty()
|
||||
)
|
||||
transaction?.finish()
|
||||
items
|
||||
}
|
||||
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
|
||||
.flowOn(dispatchers.computation)
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
|
|||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
|
|
@ -542,7 +542,7 @@ private fun TimelineItemEventRowContent(
|
|||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
senderId: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
senderProfile: ProfileDetails,
|
||||
senderAvatar: AvatarData,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -844,7 +844,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
|
|||
type = TextMessageType("This is the latest message in the thread", null)
|
||||
),
|
||||
senderId = UserId("@user:id"),
|
||||
senderProfile = ProfileTimelineDetails.Ready(
|
||||
senderProfile = ProfileDetails.Ready(
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
displayNameAmbiguous = false,
|
||||
|
|
@ -877,7 +877,7 @@ internal fun ThreadSummaryViewPreview() {
|
|||
type = TextMessageType(body, null)
|
||||
),
|
||||
senderId = UserId("@user:id"),
|
||||
senderProfile = ProfileTimelineDetails.Ready(
|
||||
senderProfile = ProfileDetails.Ready(
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
displayNameAmbiguous = true,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInv
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
|
@ -63,7 +63,7 @@ class TimelineItemContentFactory(
|
|||
eventId: EventId?,
|
||||
isEditable: Boolean,
|
||||
sender: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
senderProfile: ProfileDetails,
|
||||
): TimelineItemEventContent {
|
||||
val isOutgoing = sessionId == sender
|
||||
return when (itemContent) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
|
|
@ -69,7 +69,7 @@ sealed interface TimelineItem {
|
|||
val eventId: EventId? = null,
|
||||
val transactionId: TransactionId? = null,
|
||||
val senderId: UserId,
|
||||
val senderProfile: ProfileTimelineDetails,
|
||||
val senderProfile: ProfileDetails,
|
||||
val senderAvatar: AvatarData,
|
||||
val content: TimelineItemEventContent,
|
||||
val sentTimeMillis: Long = 0L,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject
|
|||
import dev.zacsweers.metro.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerPresenter
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
|
@ -164,25 +164,25 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: VoiceMessageComposerEvents) {
|
||||
fun handleEvent(event: VoiceMessageComposerEvent) {
|
||||
when (event) {
|
||||
is VoiceMessageComposerEvents.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent)
|
||||
is VoiceMessageComposerEvents.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent)
|
||||
is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch {
|
||||
is VoiceMessageComposerEvent.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent)
|
||||
is VoiceMessageComposerEvent.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent)
|
||||
is VoiceMessageComposerEvent.SendVoiceMessage -> localCoroutineScope.launch {
|
||||
sendVoiceMessage()
|
||||
}
|
||||
VoiceMessageComposerEvents.DeleteVoiceMessage -> {
|
||||
VoiceMessageComposerEvent.DeleteVoiceMessage -> {
|
||||
player.pause()
|
||||
localCoroutineScope.deleteRecording()
|
||||
}
|
||||
VoiceMessageComposerEvents.DismissPermissionsRationale -> {
|
||||
VoiceMessageComposerEvent.DismissPermissionsRationale -> {
|
||||
permissionState.eventSink(PermissionsEvents.CloseDialog)
|
||||
}
|
||||
VoiceMessageComposerEvents.AcceptPermissionRationale -> {
|
||||
VoiceMessageComposerEvent.AcceptPermissionRationale -> {
|
||||
permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog)
|
||||
}
|
||||
is VoiceMessageComposerEvents.LifecycleEvent -> handleLifecycleEvent(event.event)
|
||||
VoiceMessageComposerEvents.DismissSendFailureDialog -> {
|
||||
is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event)
|
||||
VoiceMessageComposerEvent.DismissSendFailureDialog -> {
|
||||
showSendFailureDialog = false
|
||||
}
|
||||
}
|
||||
|
|
@ -192,7 +192,10 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
voiceMessageState = when (val state = recorderState) {
|
||||
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(
|
||||
duration = state.elapsedTime,
|
||||
levels = state.levels.toImmutableList(),
|
||||
levels = state.levels
|
||||
// Keep only the last 128 samples for display, else we can have a crash
|
||||
.takeLast(128)
|
||||
.toImmutableList(),
|
||||
)
|
||||
is VoiceRecorderState.Finished ->
|
||||
previewState(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@
|
|||
<string name="screen_room_timeline_reactions_show_more">"نمایش بیشتر"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"نمایش خلاصهٔ واکنشها"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"جدید"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$dتغییر اتاق"</item>
|
||||
<item quantity="other">"%1$dتغییر اتاق"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_tombstoned_room_action">"پرش به اتاق جدید"</string>
|
||||
<string name="screen_room_timeline_tombstoned_room_message">"این اتاق جایگزین شده و دیگر فعّال نیست"</string>
|
||||
<string name="screen_room_timeline_upgraded_room_action">"دیدن پیامهای قدیمی"</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="emoji_picker_category_objects">"Oggetti"</string>
|
||||
<string name="emoji_picker_category_people">"Faccine & Persone"</string>
|
||||
<string name="emoji_picker_category_places">"Viaggi & Luoghi"</string>
|
||||
<string name="emoji_picker_category_recent">"Emoji recenti"</string>
|
||||
<string name="emoji_picker_category_symbols">"Simboli"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tocca per modificare la qualità di caricamento del video"</string>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Caricamento del file multimediale fallito, riprova."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"La dimensione massima consentita del file è %1$s ."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Il file è troppo grande per essere caricato"</string>
|
||||
<string name="screen_media_upload_preview_item_count">"Elemento %1$d di %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Ottimizza la qualità delle immagini"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Elaborazione…"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,18 @@
|
|||
<string name="emoji_picker_category_objects">"Objetos"</string>
|
||||
<string name="emoji_picker_category_people">"Sorrisos & Pessoas"</string>
|
||||
<string name="emoji_picker_category_places">"Viagens & Lugares"</string>
|
||||
<string name="emoji_picker_category_recent">"Emojis recentes"</string>
|
||||
<string name="emoji_picker_category_symbols">"Símbolos"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam apps mais antigos."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toque para alterar a qualidade do envio do vídeo"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"O arquivo não pôde ser enviado."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar a mídia para o envio. Tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Falha ao enviar mídia. Tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"O tamanho de arquivo máximo permitido é %1$s."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"O arquivo é muito grande para enviar"</string>
|
||||
<string name="screen_media_upload_preview_item_count">"%1$d de %2$d itens"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Otimizar qualidade da imagem"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Processando…"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquear usuário"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Marque se você deseja ocultar todas as mensagens atuais e futuras desse usuário"</string>
|
||||
<string name="screen_report_content_explanation">"Essa mensagem será reportada ao administrador do seu servidor-casa. Eles não conseguirão ler nenhuma mensagem criptografada."</string>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,15 @@
|
|||
<string name="emoji_picker_category_people">"Smayllar va odamlar"</string>
|
||||
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
|
||||
<string name="emoji_picker_category_symbols">"Belgilar"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Taglavhalar eski ilovalardan foydalanuvchilarga ko‘rinmasligi mumkin."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Video yuklash sifatini oʻzgartirish uchun bosing"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Faylni yuklab boʻlmadi."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Mediani yuklab bo‘lmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Media yuklanmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"Ruxsat etilgan maksimal fayl hajmi %1$s ."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Fayl yuklash uchun juda katta"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Tasvir sifatini optimallashtirish"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Qayta ishlanmoqda…"</string>
|
||||
<string name="screen_report_content_block_user">"Foydalanuvchini bloklash"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring"</string>
|
||||
<string name="screen_report_content_explanation">"Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi."</string>
|
||||
|
|
@ -33,16 +40,31 @@
|
|||
<string name="screen_room_timeline_add_reaction">"Emoji qo\'shmoq"</string>
|
||||
<string name="screen_room_timeline_beginning_of_room">"Bu %1$sni boshlanishi"</string>
|
||||
<string name="screen_room_timeline_beginning_of_room_no_name">"Bu suhbatning boshlanishi."</string>
|
||||
<string name="screen_room_timeline_legacy_call">"Chaqiruv qabul qilinmaydi. Chaqiruvchidan yangi Element X ilovasidan foydalanishi mumkinligini so‘rang."</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Kamroq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_message_copied">"Xabar nusxalandi"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"Sizda bu xonaga post yozishga ruxsat yo‘q"</string>
|
||||
<plurals name="screen_room_timeline_reaction_a11y">
|
||||
<item quantity="one">"%1$d ta a’zo %2$s bilan munosabat bildirdi"</item>
|
||||
<item quantity="other">"%1$d ta a’zo %2$s bilan munosabat bildirdi"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_reaction_including_you_a11y">
|
||||
<item quantity="one">"Siz va %1$d ta a’zo %2$s bilan munosabat bildirdi"</item>
|
||||
<item quantity="other">"Siz va %1$d ta a’zo %2$s bilan munosabat bildirdi"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_reaction_you_a11y">"%1$s bilan munosabat bildirdingiz"</string>
|
||||
<string name="screen_room_timeline_reactions_show_less">"Kamroq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"Ko\'proq ko\'rsatish"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Reaksiyalar xulosasini chiqarish"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Yangi"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$dxonani almashtirish"</item>
|
||||
<item quantity="other">"%1$dxona o\'zgarishi"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_tombstoned_room_action">"Yangi xonaga o‘tish"</string>
|
||||
<string name="screen_room_timeline_tombstoned_room_message">"Bu room almashtirildi va endi faol emas"</string>
|
||||
<string name="screen_room_timeline_upgraded_room_action">"Eski xabarlarni ko‘rish"</string>
|
||||
<string name="screen_room_timeline_upgraded_room_message">"Bu xona boshqa xonaning davomi"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s va %3$d boshqalar"</item>
|
||||
<item quantity="other">"%1$s, %2$s va %3$d boshqalar"</item>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
|||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
|
|
@ -1054,6 +1055,7 @@ class TimelinePresenterTest {
|
|||
typingNotificationPresenter = { aTypingNotificationState() },
|
||||
roomCallStatePresenter = { aStandByCallState() },
|
||||
featureFlagService = featureFlagService,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,10 @@ package io.element.android.features.messages.impl.voicemessages.composer
|
|||
|
||||
import android.Manifest
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent
|
||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
|
|
@ -42,10 +39,12 @@ import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
|||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
||||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -57,7 +56,7 @@ import java.io.File
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class VoiceMessageComposerPresenterTest {
|
||||
class DefaultVoiceMessageComposerPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
|
|
@ -91,9 +90,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
voiceRecorder.assertCalls(started = 0)
|
||||
|
|
@ -105,10 +102,8 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - recording state`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||
|
|
@ -118,20 +113,42 @@ class VoiceMessageComposerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - recording state - number of levels is limited`() = runTest {
|
||||
val numberOfLevels = 200
|
||||
val levels = List(numberOfLevels) { it / numberOfLevels.toFloat() }
|
||||
val voiceRecorder = FakeVoiceRecorder(
|
||||
levels = levels,
|
||||
recordingDuration = RECORDING_DURATION,
|
||||
)
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||
voiceRecorder = voiceRecorder,
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
skipItems(numberOfLevels / 2 - 1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java)
|
||||
val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording
|
||||
// The number of levels should be limited to 128 items
|
||||
assertThat(recordingState.levels.size).isEqualTo(128)
|
||||
assertThat(recordingState.levels).isEqualTo(levels.takeLast(128))
|
||||
testPauseAndDestroy(finalState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - recording keeps screen on`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
awaitItem().apply {
|
||||
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
assertThat(keepScreenOn).isFalse()
|
||||
}
|
||||
|
||||
awaitItem().apply {
|
||||
assertThat(keepScreenOn).isTrue()
|
||||
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
}
|
||||
|
||||
val finalState = awaitItem().apply {
|
||||
|
|
@ -145,11 +162,9 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - abort recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
||||
|
|
@ -160,11 +175,9 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - finish recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
||||
|
|
@ -177,12 +190,10 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - play recording before it is ready`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
val finalState = awaitItem().apply {
|
||||
this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
this.eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
}
|
||||
|
||||
// Nothing should happen
|
||||
|
|
@ -196,12 +207,10 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - play recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
val finalState = awaitItem().also {
|
||||
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
||||
}
|
||||
|
|
@ -214,13 +223,11 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - pause recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
||||
val finalState = awaitItem().also {
|
||||
assertThat(it.voiceMessageState).isEqualTo(aPausedState())
|
||||
}
|
||||
|
|
@ -233,18 +240,16 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - seek recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true))
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 5.seconds, showCursor = true))
|
||||
eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.2f)))
|
||||
eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.2f)))
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true))
|
||||
|
|
@ -260,12 +265,10 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - delete recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
|
|
@ -278,13 +281,11 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - delete while playing`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPausedState())
|
||||
}
|
||||
|
|
@ -300,12 +301,10 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - send recording`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
|
|
@ -319,21 +318,19 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - sending is tracked`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
// Send a normal voice message
|
||||
messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
skipItems(1) // Sending state
|
||||
advanceUntilIdle()
|
||||
// Now reply with a voice message
|
||||
messageComposerContext.composerMode = aReplyMode()
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
val finalState = awaitItem() // Sending state
|
||||
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
|
|
@ -348,13 +345,11 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - send while playing`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPlayingState().toSendingState())
|
||||
skipItems(1) // Duplicate sending state
|
||||
|
||||
|
|
@ -370,14 +365,12 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - send recording before previous completed, waits`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().run {
|
||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
}
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
|
||||
|
|
@ -395,20 +388,18 @@ class VoiceMessageComposerPresenterTest {
|
|||
// Let sending fail due to media preprocessing error
|
||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
}
|
||||
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
|
||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||
assertThat(analyticsService.trackedErrors).hasSize(0)
|
||||
assertThat(analyticsService.trackedErrors).isEmpty()
|
||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
||||
|
||||
testPauseAndDestroy(finalState)
|
||||
|
|
@ -419,15 +410,13 @@ class VoiceMessageComposerPresenterTest {
|
|||
fun `present - send failures can be retried`() = runTest {
|
||||
// Let sending fail due to media preprocessing error
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
val previewState = awaitItem()
|
||||
|
||||
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
|
|
@ -435,7 +424,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||
|
||||
mediaPreProcessor.givenAudioResult()
|
||||
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||
|
|
@ -448,14 +437,12 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - send failures are displayed as an error dialog`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
// Let sending fail due to media preprocessing error
|
||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
|
||||
|
|
@ -467,7 +454,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
awaitItem().apply {
|
||||
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
||||
assertThat(showSendFailureDialog).isTrue()
|
||||
eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog)
|
||||
eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog)
|
||||
}
|
||||
|
||||
val finalState = awaitItem().apply {
|
||||
|
|
@ -483,12 +470,10 @@ class VoiceMessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - send error - missing recording is tracked`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
// Send the message before recording anything
|
||||
initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||
initialState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
|
||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||
|
|
@ -504,11 +489,9 @@ class VoiceMessageComposerPresenterTest {
|
|||
val exception = SecurityException("")
|
||||
voiceRecorder.givenThrowsSecurityException(exception)
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
|
||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||
assertThat(analyticsService.trackedErrors).containsExactly(
|
||||
|
|
@ -528,19 +511,17 @@ class VoiceMessageComposerPresenterTest {
|
|||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||
permissionsPresenter = permissionsPresenter,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
|
||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
voiceRecorder.assertCalls(stopped = 1)
|
||||
|
||||
permissionsPresenter.setPermissionGranted()
|
||||
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||
voiceRecorder.assertCalls(stopped = 1, started = 1)
|
||||
|
|
@ -557,16 +538,14 @@ class VoiceMessageComposerPresenterTest {
|
|||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||
permissionsPresenter = permissionsPresenter,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
|
||||
// See the dialog and accept it
|
||||
awaitItem().also {
|
||||
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
assertThat(it.showPermissionRationaleDialog).isTrue()
|
||||
it.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale)
|
||||
it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
|
||||
}
|
||||
|
||||
// Dialog is hidden, user accepts permissions
|
||||
|
|
@ -574,7 +553,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
|
||||
permissionsPresenter.setPermissionGranted()
|
||||
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||
voiceRecorder.assertCalls(started = 1)
|
||||
|
|
@ -591,22 +570,20 @@ class VoiceMessageComposerPresenterTest {
|
|||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||
permissionsPresenter = permissionsPresenter,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
|
||||
// See the dialog and accept it
|
||||
awaitItem().also {
|
||||
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
assertThat(it.showPermissionRationaleDialog).isTrue()
|
||||
it.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale)
|
||||
it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
|
||||
}
|
||||
|
||||
// Dialog is hidden, user tries to record again
|
||||
awaitItem().also {
|
||||
assertThat(it.showPermissionRationaleDialog).isFalse()
|
||||
it.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
}
|
||||
|
||||
// Dialog is shown once again
|
||||
|
|
@ -624,7 +601,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
mostRecentState: VoiceMessageComposerState,
|
||||
) {
|
||||
mostRecentState.eventSink(
|
||||
VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE)
|
||||
VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE)
|
||||
)
|
||||
|
||||
val onPauseState = when (val state = mostRecentState.voiceMessageState) {
|
||||
|
|
@ -645,7 +622,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
}
|
||||
|
||||
onPauseState.eventSink(
|
||||
VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY)
|
||||
VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY)
|
||||
)
|
||||
|
||||
when (val state = onPauseState.voiceMessageState) {
|
||||
|
|
@ -662,6 +639,7 @@ class VoiceMessageComposerPresenterTest {
|
|||
|
||||
private fun TestScope.createDefaultVoiceMessageComposerPresenter(
|
||||
permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(),
|
||||
voiceRecorder: VoiceRecorder = this@DefaultVoiceMessageComposerPresenterTest.voiceRecorder,
|
||||
): DefaultVoiceMessageComposerPresenter {
|
||||
return DefaultVoiceMessageComposerPresenter(
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
|
|
@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.core.UniqueId
|
|||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
|
|
@ -86,7 +86,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
|||
reactions = persistentListOf(),
|
||||
receipts = persistentListOf(),
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileTimelineDetails.Unavailable,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
timestamp = 9442,
|
||||
content = RedactedContent,
|
||||
origin = null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="a11y_polls_percent_of_total">
|
||||
<item quantity="one">"Jami ovozlarning %1$d foizi"</item>
|
||||
<item quantity="other">"Jami ovozlarning %1$d foizi"</item>
|
||||
</plurals>
|
||||
<string name="a11y_polls_will_remove_selection">"Oldingi tanlov olib tashlanadi"</string>
|
||||
<string name="a11y_polls_winning_answer">"Bu g\'alaba qozongan javob"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<string name="screen_create_poll_anonymous_headline">"Ovozlarni yashirish"</string>
|
||||
<string name="screen_create_poll_answer_hint">"Variant%1$d"</string>
|
||||
<string name="screen_create_poll_cancel_confirmation_content_android">"Oʻzgarishlar saqlanmadi. Haqiqatan ham orqaga qaytmoqchimisiz?"</string>
|
||||
<string name="screen_create_poll_delete_option_a11y">"%1$s variantini o‘chirish"</string>
|
||||
<string name="screen_create_poll_question_desc">"Savol yoki mavzu"</string>
|
||||
<string name="screen_create_poll_question_hint">"So\'rovnoma nima haqida?"</string>
|
||||
<string name="screen_create_poll_title">"So‘rovnoma yaratish"</string>
|
||||
|
|
|
|||
|
|
@ -186,11 +186,20 @@ class PreferencesFlowNode(
|
|||
override fun navigateToPushHistory() {
|
||||
backstack.push(NavTarget.PushHistory)
|
||||
}
|
||||
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<DeveloperSettingsNode>(buildContext, listOf(developerSettingsCallback))
|
||||
}
|
||||
NavTarget.Labs -> {
|
||||
createNode<LabsNode>(buildContext)
|
||||
val callback = object : LabsNode.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<LabsNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.About -> {
|
||||
val callback = object : AboutNode.Callback {
|
||||
|
|
@ -267,7 +276,12 @@ class PreferencesFlowNode(
|
|||
}
|
||||
is NavTarget.UserProfile -> {
|
||||
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
|
||||
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
||||
val callback = object : EditUserProfileNode.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<EditUserProfileNode>(buildContext, listOf(inputs, callback))
|
||||
}
|
||||
NavTarget.LockScreenSettings -> {
|
||||
lockScreenEntryPoint.createNode(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class DeveloperSettingsNode(
|
|||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun navigateToPushHistory()
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
|
@ -49,7 +50,7 @@ class DeveloperSettingsNode(
|
|||
modifier = modifier,
|
||||
onOpenShowkase = ::openShowkase,
|
||||
onPushHistoryClick = callback::navigateToPushHistory,
|
||||
onBackClick = ::navigateUp
|
||||
onBackClick = callback::onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -25,9 +26,18 @@ class LabsNode(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: LabsPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
val callback: Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
LabsView(state = state, onBack = ::navigateUp)
|
||||
LabsView(
|
||||
state = state,
|
||||
onBack = callback::onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
|||
sealed interface EditUserProfileEvents {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
|
||||
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
|
||||
data object Exit : EditUserProfileEvents
|
||||
data object Save : EditUserProfileEvents
|
||||
data object CancelSaveChanges : EditUserProfileEvents
|
||||
data object CloseDialog : EditUserProfileEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
interface EditUserProfileNavigator {
|
||||
fun close()
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -27,22 +28,32 @@ class EditUserProfileNode(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: EditUserProfilePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
EditUserProfileNavigator {
|
||||
data class Inputs(
|
||||
val matrixUser: MatrixUser
|
||||
) : NodeInputs
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
val matrixUser = inputs<Inputs>().matrixUser
|
||||
val presenter = presenterFactory.create(matrixUser)
|
||||
val callback: Callback = callback()
|
||||
val presenter = presenterFactory.create(
|
||||
matrixUser = matrixUser,
|
||||
navigator = this,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
EditUserProfileView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onEditProfileSuccess = ::navigateUp,
|
||||
onEditProfileSuccess = ::close,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() = callback.onDone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import timber.log.Timber
|
|||
@AssistedInject
|
||||
class EditUserProfilePresenter(
|
||||
@Assisted private val matrixUser: MatrixUser,
|
||||
@Assisted private val navigator: EditUserProfileNavigator,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPickerProvider: PickerProvider,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
|
|
@ -57,7 +58,10 @@ class EditUserProfilePresenter(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(matrixUser: MatrixUser): EditUserProfilePresenter
|
||||
fun create(
|
||||
matrixUser: MatrixUser,
|
||||
navigator: EditUserProfileNavigator,
|
||||
): EditUserProfilePresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -101,6 +105,13 @@ class EditUserProfilePresenter(
|
|||
|
||||
val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val canSave = remember(userDisplayName, userAvatarUri) {
|
||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) ||
|
||||
hasAvatarUrlChanged(userAvatarUri, matrixUser)
|
||||
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
||||
}
|
||||
|
||||
fun handleEvent(event: EditUserProfileEvents) {
|
||||
when (event) {
|
||||
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(
|
||||
|
|
@ -124,18 +135,32 @@ class EditUserProfilePresenter(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
|
||||
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = AsyncAction.Uninitialized
|
||||
EditUserProfileEvents.Exit -> {
|
||||
when (saveAction.value) {
|
||||
is AsyncAction.Confirming -> {
|
||||
// Close the dialog right now
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
navigator.close()
|
||||
}
|
||||
AsyncAction.Loading -> Unit
|
||||
is AsyncAction.Failure,
|
||||
is AsyncAction.Success -> {
|
||||
// Should not happen
|
||||
}
|
||||
AsyncAction.Uninitialized -> {
|
||||
if (canSave) {
|
||||
saveAction.value = AsyncAction.ConfirmingCancellation
|
||||
} else {
|
||||
navigator.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EditUserProfileEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
val canSave = remember(userDisplayName, userAvatarUri) {
|
||||
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) ||
|
||||
hasAvatarUrlChanged(userAvatarUri, matrixUser)
|
||||
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
||||
}
|
||||
|
||||
return EditUserProfileState(
|
||||
userId = matrixUser.userId,
|
||||
displayName = userDisplayName.orEmpty(),
|
||||
|
|
|
|||
|
|
@ -11,27 +11,36 @@ package io.element.android.features.preferences.impl.user.editprofile
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
|
||||
override val values: Sequence<EditUserProfileState>
|
||||
get() = sequenceOf(
|
||||
aEditUserProfileState(),
|
||||
aEditUserProfileState(userAvatarUrl = "example://uri"),
|
||||
// Add other states here
|
||||
aEditUserProfileState(saveAction = AsyncAction.ConfirmingCancellation),
|
||||
)
|
||||
}
|
||||
|
||||
fun aEditUserProfileState(
|
||||
userId: UserId = UserId("@john.doe:matrix.org"),
|
||||
displayName: String = "John Doe",
|
||||
userAvatarUrl: String? = null,
|
||||
avatarActions: List<AvatarAction> = emptyList(),
|
||||
saveButtonEnabled: Boolean = true,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||
eventSink: (EditUserProfileEvents) -> Unit = {},
|
||||
) = EditUserProfileState(
|
||||
userId = UserId("@john.doe:matrix.org"),
|
||||
displayName = "John Doe",
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
userAvatarUrl = userAvatarUrl,
|
||||
avatarActions = persistentListOf(),
|
||||
saveAction = AsyncAction.Uninitialized,
|
||||
saveButtonEnabled = true,
|
||||
cameraPermissionState = aPermissionsState(showDialog = false),
|
||||
eventSink = {}
|
||||
avatarActions = avatarActions.toImmutableList(),
|
||||
saveButtonEnabled = saveButtonEnabled,
|
||||
saveAction = saveAction,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -30,11 +31,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.SaveChangesDialog
|
||||
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -52,7 +55,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun EditUserProfileView(
|
||||
state: EditUserProfileState,
|
||||
onBackClick: () -> Unit,
|
||||
onEditProfileSuccess: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -64,12 +66,21 @@ fun EditUserProfileView(
|
|||
isAvatarActionsSheetVisible.value = true
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(EditUserProfileEvents.Exit)
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = true,
|
||||
::onBackClick,
|
||||
)
|
||||
Scaffold(
|
||||
modifier = modifier.clearFocusOnTap(focusManager),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(R.string.screen_edit_profile_title),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
navigationIcon = { BackButton(::onBackClick) },
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
|
|
@ -132,10 +143,20 @@ fun EditUserProfileView(
|
|||
progressText = stringResource(R.string.screen_edit_profile_updating_details),
|
||||
)
|
||||
},
|
||||
confirmationDialog = { confirming ->
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation -> {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(EditUserProfileEvents.Exit) },
|
||||
onDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { onEditProfileSuccess() },
|
||||
errorTitle = { stringResource(R.string.screen_edit_profile_error_title) },
|
||||
errorMessage = { stringResource(R.string.screen_edit_profile_error) },
|
||||
onErrorDismiss = { state.eventSink(EditUserProfileEvents.CancelSaveChanges) },
|
||||
onErrorDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) },
|
||||
)
|
||||
}
|
||||
PermissionsView(
|
||||
|
|
@ -148,7 +169,6 @@ fun EditUserProfileView(
|
|||
internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
|
||||
ElementPreview {
|
||||
EditUserProfileView(
|
||||
onBackClick = {},
|
||||
onEditProfileSuccess = {},
|
||||
state = state,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto."</string>
|
||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Nascondi gli avatar nelle richieste di invito alle stanze"</string>
|
||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Nascondi le anteprime dei media nelle conversazioni"</string>
|
||||
<string name="screen_advanced_settings_labs">"Labs"</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Carica foto e video più velocemente e riduci l\'utilizzo dei dati"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Ottimizza la qualità dei contenuti multimediali"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderazione e Sicurezza"</string>
|
||||
|
|
@ -43,6 +44,11 @@
|
|||
<string name="screen_edit_profile_error_title">"Impossibile aggiornare il profilo"</string>
|
||||
<string name="screen_edit_profile_title">"Modifica profilo"</string>
|
||||
<string name="screen_edit_profile_updating_details">"Aggiornamento del profilo…"</string>
|
||||
<string name="screen_labs_enable_threads">"Abilita le risposte alle discussioni"</string>
|
||||
<string name="screen_labs_enable_threads_description">"L\'app si riavvierà per applicare questa modifica."</string>
|
||||
<string name="screen_labs_header_description">"Prova le nostre ultime idee in fase di sviluppo. Queste funzionalità non sono definitive; potrebbero essere instabili e soggette a modifiche."</string>
|
||||
<string name="screen_labs_header_title">"Hai voglia di sperimentare?"</string>
|
||||
<string name="screen_labs_title">"Labs"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Impostazioni aggiuntive"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Chiamate audio e video"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Mancata corrispondenza di configurazione"</string>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL inválida, por favor verifique se o protocolo (http/https) está incluso e o endereço correto."</string>
|
||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ocultar avatares em solicitações de convite para salas"</string>
|
||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ocultar pré-visualizações de mídia na linha do tempo"</string>
|
||||
<string name="screen_advanced_settings_labs">"Experimentos"</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Envie fotos e vídeos com mais rapidez e reduza o uso de dados"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Otimizar a qualidade da mídia"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderação e segurança"</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Otimizar automaticamente as imagens para envios mais rápidos e arquivos com tamanhos menores."</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Otimizar qualidade de envio de imagens"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Toque aqui para alterar."</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Alta (1080p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Baixa (480p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Normal (720p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualidade de envio de vídeos"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Provedor de notificações push"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Desative o editor de rich text para digitar Markdown manualmente."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Confirmações de leitura"</string>
|
||||
|
|
@ -36,6 +44,11 @@
|
|||
<string name="screen_edit_profile_error_title">"Não foi possível atualizar o perfil"</string>
|
||||
<string name="screen_edit_profile_title">"Editar perfil"</string>
|
||||
<string name="screen_edit_profile_updating_details">"Atualizando o perfil…"</string>
|
||||
<string name="screen_labs_enable_threads">"Ativar respostas de tópicos"</string>
|
||||
<string name="screen_labs_enable_threads_description">"O app será reiniciado para aplicar esta mudança."</string>
|
||||
<string name="screen_labs_header_description">"Teste as nossas mais novas ideias em desenvolvimento. Esses recursos não estão finalizados; podem estar instáveis, e podem mudar."</string>
|
||||
<string name="screen_labs_header_title">"Se sentindo experimental?"</string>
|
||||
<string name="screen_labs_title">"Experimentos"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Configurações adicionais"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Chamadas de áudio e vídeo"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Não correspondência de configuração"</string>
|
||||
|
|
|
|||
|
|
@ -8,14 +8,29 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url">"Maxsus element qo‘ng‘iroqlar bazasi URL manzili"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Element qo\'ng\'irog\'iga maxsus asosiy url or\'natish"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL noto‘g‘ri, iltimos, protokol (http/https) va to‘g‘ri manzilni kiritganingizga ishonch hosil qiling."</string>
|
||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Xonaga taklif so‘rovlarida avatarlarni berkitish"</string>
|
||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Vaqt jadvalida mediaga razm solishlarni berkitish"</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Rasm va videolarni tezroq yuklang va trafik sarfini kamaytiring"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Media sifatini yaxshilash"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderatsiya va xavfsizlik"</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Tezroq yuklash va kichikroq fayl hajmi uchun rasmlarni avtomatik optimallashtirish."</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Rasm yuklash sifatini optimallashtirish"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Oʻzgartirish uchun bu yerga bosing."</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Yuqori (1080p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Past (480p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standart (720p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Video yuklash sifati"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Push bildirishnoma provayderi"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Boy matn muharriri o\'chiring Markdown bilan qo\'lda yozish uchun"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Kvitansiyalarni oʻqish"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts_description">"Agar oʻchirib qo‘yilsa, sizning oʻqilganlik bildirishnomangiz hech kimga yuborilmaydi. Siz boshqa foydalanuvchilardan oʻqilganlik bildirishnomalarini olishda davom etasiz."</string>
|
||||
<string name="screen_advanced_settings_share_presence">"Mavjudligini ulashish"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Agar oʻchirib qoʻyilsa, siz oʻqilganlik haqidagi bildirishnomalarni yoki yozayotganingiz haqidagi xabarlarni yubora olmaysiz va qabul qila olmaysiz."</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Doim berkitilsin"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_always_show">"Har doim ko‘rsatish"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Shaxsiy xonalarda"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Yashirin media har doim unga bosish orqali ko‘rsatilishi mumkin"</string>
|
||||
<string name="screen_advanced_settings_show_media_timeline_title">"Vaqt jadvalida media ko‘rsatish"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Xabar manbasini vaqt jadvalida ko‘rish imkoniyatini yoqing."</string>
|
||||
<string name="screen_blocked_users_empty">"Sizda bloklangan foydalanuvchi yo‘q"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Blokdan chiqarish"</string>
|
||||
|
|
@ -55,6 +70,7 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin."</string>
|
|||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"tizim sozlamalari"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"Tizim bildirishnomalari o\'chirilgan"</string>
|
||||
<string name="screen_notification_settings_title">"Bildirishnomalar"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_push_history_title">"Bildirishnoma tarixi"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_section">"Muammolarni bartaraf etish"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_title">"Bildirishnomalar bilan bog‘liq muammolarni bartaraf etish"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@
|
|||
<string name="screen_edit_profile_error_title">"无法更新个人资料"</string>
|
||||
<string name="screen_edit_profile_title">"编辑个人资料"</string>
|
||||
<string name="screen_edit_profile_updating_details">"更新个人资料……"</string>
|
||||
<string name="screen_labs_enable_threads">"启用主题回复"</string>
|
||||
<string name="screen_labs_enable_threads_description">"应用将重启以应用此更改。"</string>
|
||||
<string name="screen_labs_header_description">"尝试我们最新的开发理念。这些功能尚未最终确定,可能不稳定,也可能会发生变化。"</string>
|
||||
<string name="screen_labs_header_title">"想尝试新功能?"</string>
|
||||
<string name="screen_labs_title">"实验室"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"更多设置"</string>
|
||||
<string name="screen_notification_settings_calls_label">"音视频通话"</string>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue