Merge branch 'release/0.6.5' into main

This commit is contained in:
Benoit Marty 2024-10-09 09:39:07 +02:00
commit 996147eaa6
2144 changed files with 10352 additions and 6873 deletions

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
debug:
@ -30,11 +30,11 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
build:
@ -38,11 +38,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -13,11 +13,11 @@ jobs:
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.2
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -13,13 +13,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
name: Use JDK 17
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1
uses: gradle-update/update-gradle-wrapper-action@v2
with:
repo-token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
target-branch: develop

View file

@ -8,8 +8,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
build-apk:
@ -33,11 +33,11 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@v4
name: Use JDK 17
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -7,8 +7,8 @@ on:
- cron: "0 4 * * *"
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
nightly:
@ -17,11 +17,11 @@ jobs:
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Build and upload Nightly application
run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES

View file

@ -8,8 +8,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
nightlyReports:
@ -20,11 +20,11 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.2
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
@ -61,11 +61,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -7,8 +7,8 @@ on:
- cron: "0 4 * * *"
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
nightly:
@ -23,11 +23,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Build and upload Nightly application
run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
checkScript:
@ -46,11 +46,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
@ -84,11 +84,11 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
@ -124,11 +124,11 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
@ -168,11 +168,11 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
@ -208,11 +208,11 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
@ -248,11 +248,11 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -7,7 +7,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache
jobs:
record:
@ -32,11 +33,11 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2
with:
persist-credentials: false
- name: ☕️ Use JDK 17
- name: ☕️ Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
@ -48,3 +49,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}
GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }}

View file

@ -7,8 +7,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
gplay:
@ -19,11 +19,11 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
- name: Create app bundle
@ -55,11 +55,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Enterprise app bundle
@ -83,11 +83,11 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
- name: Create APKs

View file

@ -51,10 +51,10 @@ if [[ -z ${REPO} ]]; then
fi
echo "Deleting previous screenshots"
./gradlew removeOldSnapshots --stacktrace --warn
./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS
echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
echo "Committing changes"
git config http.sslVerify false

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache
GROUP: ${{ format('sonar-{0}', github.ref) }}
jobs:
@ -27,11 +27,11 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -12,11 +12,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
- name: Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
tests:
@ -46,24 +46,18 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 17
- name: ☕️ Use JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit tests for debug variant
run: ./gradlew testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
- name: 📸 Run screenshot tests
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports
if: failure()
@ -71,7 +65,7 @@ jobs:
with:
name: kover-error-report
path: |
app/build/reports/kover/verifyGplayDebug.err
app/build/reports/kover
- name: ✅ Upload kover report (disabled)
if: always()

1
.gitignore vendored
View file

@ -53,6 +53,7 @@ captures/
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
.idea/other.xml
.idea/runConfigurations.xml
.idea/tasks.xml
.idea/workspace.xml
.idea/libraries

View file

@ -1,3 +1,39 @@
Changes in Element X v0.6.4 (2024-09-25)
========================================
### 🙌 Improvements
* Pinned messages : add pin icon in timeline for pinned events. by @ganfra in https://github.com/element-hq/element-x-android/pull/3500
* Include inviter in the notification for invitation by @bmarty in https://github.com/element-hq/element-x-android/pull/3503
### 🐛 Bugfixes
* Fix crash when session is deleted on another client by @bmarty in https://github.com/element-hq/element-x-android/pull/3515
* Fix pinned events banner reappearing when loading by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3519
* Fix various crashes by @bmarty in https://github.com/element-hq/element-x-android/pull/3533
* Perform the migration, even if the current version is not known. by @bmarty in https://github.com/element-hq/element-x-android/pull/3535
* timeline : makes sure to emit empty list if initial reset has no item. by @ganfra in https://github.com/element-hq/element-x-android/pull/3538
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3513
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3517
### Dependency upgrades
* Update dependency io.nlopez.compose.rules:detekt to v0.4.12 by @renovate in https://github.com/element-hq/element-x-android/pull/3436
* Update dependency com.posthog:posthog-android to v3.7.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3443
* Update dependency com.otaliastudios:transcoder to v0.11.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3440
* Update dependency org.maplibre.gl:android-sdk to v11.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3408
* Update dependencyAnalysis to v2.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3508
* Update dependency org.maplibre.gl:android-sdk-ktx-v7 to v3.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3507
* Update dependencyAnalysis to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3526
* Update dependency net.java.dev.jna:jna to v5.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3525
* Update dependency androidx.startup:startup-runtime to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3516
* dependencies : update rust sdk to 0.2.48 by @ganfra in https://github.com/element-hq/element-x-android/pull/3532
### Others
* Change ElementBot mail to android@element.io by @bmarty in https://github.com/element-hq/element-x-android/pull/3497
* Test RustMatrixClient and other classes in the matrix module by @bmarty in https://github.com/element-hq/element-x-android/pull/3501
* Pinned messages analytics by @ganfra in https://github.com/element-hq/element-x-android/pull/3523
* Remove ability to configure default log level by @bmarty in https://github.com/element-hq/element-x-android/pull/3531
Changes in Element X v0.6.3 (2024-09-19)
========================================

View file

@ -7,7 +7,6 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kapt)
}
dependencies {
@ -16,6 +15,6 @@ dependencies {
implementation(libs.anvil.compiler.utils)
implementation(libs.kotlinpoet)
implementation(libs.dagger)
compileOnly(libs.google.autoservice.annotations)
kapt(libs.google.autoservice)
implementation(libs.ksp.plugin)
implementation(libs.kotlinpoet.ksp)
}

View file

@ -1,144 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
@file:OptIn(ExperimentalAnvilApi::class)
package io.element.android.anvilcodegen
import com.google.auto.service.AutoService
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.ExperimentalAnvilApi
import com.squareup.anvil.compiler.api.AnvilCompilationException
import com.squareup.anvil.compiler.api.AnvilContext
import com.squareup.anvil.compiler.api.CodeGenerator
import com.squareup.anvil.compiler.api.GeneratedFile
import com.squareup.anvil.compiler.api.createGeneratedFile
import com.squareup.anvil.compiler.internal.asClassName
import com.squareup.anvil.compiler.internal.buildFile
import com.squareup.anvil.compiler.internal.fqName
import com.squareup.anvil.compiler.internal.reference.ClassReference
import com.squareup.anvil.compiler.internal.reference.asClassName
import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import dagger.Binds
import dagger.Module
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.anvilannotations.ContributesNode
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
/**
* This is an anvil plugin that allows Node to use [ContributesNode] alone and let this plugin automatically
* handle the rest of the Dagger wiring required for constructor injection.
*/
@AutoService(CodeGenerator::class)
class ContributesNodeCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true
override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> {
return projectFiles.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(ContributesNode::class.fqName) }
.flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) }
.toList()
}
private fun generateModule(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = nodeClass.packageFqName.toString()
val moduleClassName = "${nodeClass.shortName}_Module"
val scope = nodeClass.annotations.single { it.fqName == ContributesNode::class.fqName }.scope()
val content = FileSpec.buildFile(generatedPackage, moduleClassName) {
addType(
TypeSpec.classBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build())
.addFunction(
FunSpec.builder("bind${nodeClass.shortName}Factory")
.addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(generatedPackage, "${nodeClass.shortName}_AssistedFactory"))
.returns(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(STAR))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
.addAnnotation(
AnnotationSpec.Companion.builder(nodeKeyFqName.asClassName(module)).addMember(
"%T::class",
nodeClass.asClassName()
).build()
)
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content)
}
private fun generateAssistedFactory(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = nodeClass.packageFqName.toString()
val assistedFactoryClassName = "${nodeClass.shortName}_AssistedFactory"
val constructor = nodeClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) }
val assistedParameters = constructor?.parameters?.filter { it.isAnnotatedWith(Assisted::class.fqName) }.orEmpty()
if (constructor == null || assistedParameters.size != 2) {
throw AnvilCompilationException(
"${nodeClass.fqName} must have an @AssistedInject constructor with 2 @Assisted parameters",
element = nodeClass.clazz,
)
}
val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name != "buildContext") {
throw AnvilCompilationException(
"${nodeClass.fqName} @Assisted parameter must be named buildContext",
element = contextAssistedParam.parameter,
)
}
val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name != "plugins") {
throw AnvilCompilationException(
"${nodeClass.fqName} @Assisted parameter must be named plugins",
element = pluginsAssistedParam.parameter,
)
}
val nodeClassName = nodeClass.asClassName()
val buildContextClassName = contextAssistedParam.type().asTypeName()
val pluginsClassName = pluginsAssistedParam.type().asTypeName()
val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) {
addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(nodeClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
.addParameter("buildContext", buildContextClassName)
.addParameter("plugins", pluginsClassName)
.returns(nodeClassName)
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content)
}
companion object {
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.anvilcodegen
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dagger.Binds
import dagger.Module
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.anvilannotations.ContributesNode
import org.jetbrains.kotlin.name.FqName
class ContributesNodeProcessor(
private val logger: KSPLogger,
private val codeGenerator: CodeGenerator,
private val config: Config,
) : SymbolProcessor {
data class Config(
val enableLogging: Boolean = false,
)
override fun process(resolver: Resolver): List<KSAnnotated> {
val annotatedSymbols = resolver.getSymbolsWithAnnotation(ContributesNode::class.qualifiedName!!)
.filterIsInstance<KSClassDeclaration>()
val (validSymbols, invalidSymbols) = annotatedSymbols.partition { it.validate() }
if (validSymbols.isEmpty()) return invalidSymbols
for (ksClass in validSymbols) {
if (config.enableLogging) {
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
}
generateModule(ksClass)
generateFactory(ksClass)
}
return invalidSymbols
}
private fun generateModule(ksClass: KSClassDeclaration) {
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
val modulePackage = ksClass.packageName.asString()
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
val content = FileSpec.builder(
packageName = modulePackage,
fileName = moduleClassName,
)
.addType(
TypeSpec.classBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
.addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
.addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory"))
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
.addAnnotation(
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
"%T::class",
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
).build()
)
.build(),
)
.build(),
)
.build()
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
aggregating = true,
ksClass.containingFile!!
),
)
}
@OptIn(KspExperimental::class)
private fun generateFactory(ksClass: KSClassDeclaration) {
val generatedPackage = ksClass.packageName.asString()
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) }
val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty()
if (constructor == null || assistedParameters.size != 2) {
error(
"${ksClass.qualifiedName} must have an @AssistedInject constructor with 2 @Assisted parameters",
)
}
val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name?.asString() != "buildContext") {
error(
"${ksClass.qualifiedName} @Assisted parameter must be named buildContext",
)
}
val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name?.asString() != "plugins") {
error(
"${ksClass.qualifiedName} @Assisted parameter must be named plugins",
)
}
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val buildContextClassName = contextAssistedParam.type.toTypeName()
val pluginsClassName = pluginsAssistedParam.type.toTypeName()
val content = FileSpec.builder(generatedPackage, assistedFactoryClassName)
.addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
.addParameter("buildContext", buildContextClassName)
.addParameter("plugins", pluginsClassName)
.returns(nodeClassName)
.build(),
)
.build(),
)
.build()
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
aggregating = true,
ksClass.containingFile!!
),
)
}
companion object {
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.anvilcodegen
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class ContributesNodeProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false
return ContributesNodeProcessor(
logger = environment.logger,
codeGenerator = environment.codeGenerator,
config = ContributesNodeProcessor.Config(enableLogging = enableLogging),
)
}
}

View file

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

View file

@ -11,6 +11,7 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig
import extension.AssetCopyTask
import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource
import extension.allEnterpriseImpl
@ -19,14 +20,13 @@ import extension.allLibrariesImpl
import extension.allServicesImpl
import extension.koverDependencies
import extension.locales
import extension.setupAnvil
import extension.setupKover
import java.util.Locale
plugins {
id("io.element.android-compose-application")
alias(libs.plugins.kotlin.android)
alias(libs.plugins.anvil)
alias(libs.plugins.kapt)
// When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit)
@ -161,9 +161,6 @@ android {
}
}
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
@ -233,27 +230,34 @@ knit {
}
}
setupAnvil(
generateDaggerCode = true,
generateDaggerFactoriesUsingAnvil = false,
componentMergingStrategy = ComponentMergingStrategy.KSP,
)
dependencies {
allLibrariesImpl()
allServicesImpl()
if (isEnterpriseBuild) {
allEnterpriseImpl(rootDir, logger)
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
} else {
implementation(projects.appicon.element)
}
allFeaturesImpl(rootDir, logger)
allFeaturesImpl(project)
implementation(projects.features.migration.api)
implementation(projects.anvilannotations)
implementation(projects.appnav)
implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
anvil(projects.anvilcodegen)
implementation(projects.services.analytics.compose)
// Comment to not include firebase in the project
"gplayImplementation"(projects.libraries.pushproviders.firebase)
// Comment to not include unified push in the project
implementation(projects.libraries.pushproviders.unifiedpush)
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
"gplayImplementation"(projects.libraries.pushproviders.firebase)
}
if (ModulesConfig.pushProvidersConfig.includeUnifiedPush) {
implementation(projects.libraries.pushproviders.unifiedpush)
}
implementation(libs.appyx.core)
implementation(libs.androidx.splash)
@ -272,9 +276,6 @@ dependencies {
implementation(libs.matrix.emojibase.bindings)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
@ -297,14 +298,13 @@ tasks.withType<GenerateBuildConfig>().configureEach {
licensee {
allow("Apache-2.0")
allow("MIT")
allow("GPL-2.0-with-classpath-exception")
allow("BSD-2-Clause")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("http://openjdk.java.net/legal/gplv2+ce.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")
allowUrl("https://jsoup.org/license")
allowUrl("https://asm.ow2.io/license.html")
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
}

View file

@ -10,7 +10,6 @@ package io.element.android.x.di
import android.content.Context
import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance
import dagger.Component
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@ -19,7 +18,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings {
@Component.Factory
@MergeComponent.Factory
interface Factory {
fun create(
@ApplicationContext @BindsInstance

View file

@ -10,7 +10,6 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class)
interface RoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
@BindsInstance
fun room(room: MatrixRoom): Builder

View file

@ -10,7 +10,6 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
@BindsInstance
fun client(matrixClient: MatrixClient): Builder

View file

@ -8,6 +8,7 @@
<locale android:name="en"/>
<locale android:name="es"/>
<locale android:name="et"/>
<locale android:name="fa"/>
<locale android:name="fr"/>
<locale android:name="hu"/>
<locale android:name="in"/>

View file

@ -7,6 +7,7 @@
package io.element.android.appconfig
object SecureBackupConfig {
const val LEARN_MORE_URL: String = "https://element.io/help#encryption5"
object LearnMoreConfig {
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
}

View file

@ -8,11 +8,10 @@
@file:Suppress("UnstableApiUsage")
import extension.allFeaturesApi
import extension.setupAnvil
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.kapt)
id("kotlin-parcelize")
}
@ -20,13 +19,10 @@ android {
namespace = "io.element.android.appnav"
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
setupAnvil()
allFeaturesApi(rootDir, logger)
dependencies {
allFeaturesApi(project)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)

View file

@ -12,8 +12,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import im.vector.app.features.analytics.plan.SuperProperties
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.features.share.api.ShareService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.SdkMetadata
@ -22,8 +22,8 @@ import io.element.android.services.apperror.api.AppErrorStateService
import javax.inject.Inject
class RootPresenter @Inject constructor(
private val crashDetectionPresenter: CrashDetectionPresenter,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
private val crashDetectionPresenter: Presenter<CrashDetectionState>,
private val rageshakeDetectionPresenter: Presenter<RageshakeDetectionState>,
private val appErrorStateService: AppErrorStateService,
private val analyticsService: AnalyticsService,
private val shareService: ShareService,

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш хатні сервер больш не падтрымлівае стары пратакол. Калі ласка, выйдзіце і ўвайдзіце зноў, каб працягнуць выкарыстанне праграмы."</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Esci e aggiorna"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Il tuo homeserver non supporta più il vecchio protocollo. Esci e rientra per continuare a usare l\'app."</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Wyloguj się i zaktualizuj"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Twój serwer domowy już nie wspiera starego protokołu. Zaloguj się ponownie, aby kontynuować korzystanie z aplikacji."</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Sair &amp; Atualizar"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"登出并升级"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。"</string>
</resources>

View file

@ -12,17 +12,11 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.root.RootPresenter
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
import io.element.android.features.share.api.ShareService
import io.element.android.features.share.test.FakeShareService
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService
@ -44,7 +38,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.crashDetectionState.crashDetected).isFalse()
}
@ -61,7 +54,7 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(2)
skipItems(1)
lambda.assertions().isCalledOnce()
}
}
@ -76,8 +69,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java)
val initialErrorState = initialState.errorState as AppErrorState.Error
@ -93,25 +84,9 @@ class RootPresenterTest {
appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
shareService: ShareService = FakeShareService {},
): RootPresenter {
val crashDataStore = FakeCrashDataStore()
val rageshakeDataStore = FakeRageshakeDataStore()
val rageshake = FakeRageShake()
val screenshotHolder = FakeScreenshotHolder()
val crashDetectionPresenter = DefaultCrashDetectionPresenter(
buildMeta = aBuildMeta(),
crashDataStore = crashDataStore
)
val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter(
screenshotHolder = screenshotHolder,
rageShake = rageshake,
preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore,
)
)
return RootPresenter(
crashDetectionPresenter = crashDetectionPresenter,
rageshakeDetectionPresenter = rageshakeDetectionPresenter,
crashDetectionPresenter = { aCrashDetectionState() },
rageshakeDetectionPresenter = { aRageshakeDetectionState() },
appErrorStateService = appErrorService,
analyticsService = FakeAnalyticsService(),
shareService = shareService,

View file

@ -48,7 +48,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.12")
detektPlugins("io.nlopez.compose.rules:detekt:0.4.15")
}
// KtLint
@ -71,8 +71,9 @@ allprojects {
// To have XML report for Danger
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
filter {
exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") }
exclude { element -> element.file.path.contains(generatedPath) }
}
}
// Dependency check

@ -1 +1 @@
Subproject commit ceb65e32d95052c028a37654a4a0410639f69053
Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea

View file

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

View file

@ -1,12 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.analytics.api.preferences
import io.element.android.libraries.architecture.Presenter
interface AnalyticsPreferencesPresenter : Presenter<AnalyticsPreferencesState>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_read_terms_content_link">"این‌جا"</string>
<string name="screen_analytics_settings_share_data">"هم رسانی داده‌های تحلیلی"</string>
</resources>

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.analytics.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -36,6 +35,7 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -104,14 +104,9 @@ private fun AnalyticsOptInHeader(
bold = true,
tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK,
)
ClickableText(
text = text,
onClick = {
text
.getStringAnnotations(LINK_TAG, it, it)
.firstOrNull()
?.let { _ -> onClickTerms() }
},
ClickableLinkText(
annotatedString = text,
onClick = { onClickTerms() },
modifier = Modifier
.padding(8.dp),
style = ElementTheme.typography.fontBodyMdRegular

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.analytics.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
@Module
interface AnalyticsModule {
@Binds
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
}

View file

@ -10,23 +10,20 @@ package io.element.android.features.analytics.impl.preferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultAnalyticsPreferencesPresenter @Inject constructor(
class AnalyticsPreferencesPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta,
) : AnalyticsPreferencesPresenter {
) : Presenter<AnalyticsPreferencesState> {
@Composable
override fun present(): AnalyticsPreferencesState {
val localCoroutineScope = rememberCoroutineScope()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_read_terms_content_link">"این‌جا"</string>
<string name="screen_analytics_prompt_settings">"می‌توانید در هر زمان خاموشش کنید"</string>
<string name="screen_analytics_prompt_third_party_sharing">"داده‌هایتان را با سوم‌شخص‌ها هم‌نمی‌رسانیم"</string>
<string name="screen_analytics_prompt_title">"کمک به بهبود %1$s"</string>
</resources>

View file

@ -25,7 +25,7 @@ class AnalyticsPreferencesPresenterTest {
@Test
fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta()
)
@ -41,7 +41,7 @@ class AnalyticsPreferencesPresenterTest {
@Test
fun `present - initial state not available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
aBuildMeta()
)
@ -55,7 +55,7 @@ class AnalyticsPreferencesPresenterTest {
@Test
fun `present - enable and disable`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta()
)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,20 +9,15 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.features.cachecleaner.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.cachecleaner.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
alias(libs.plugins.kotlin.serialization)
}
@ -24,13 +25,10 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.appconfig)
implementation(projects.anvilannotations)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)

View file

@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
sealed interface CallScreenEvents {
data object Hangup : CallScreenEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) :
CallScreenEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
data class OnWebViewError(val description: String?) : CallScreenEvents
}

View file

@ -78,6 +78,8 @@ class CallScreenPresenter @AssistedInject constructor(
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
DisposableEffect(Unit) {
@ -125,6 +127,8 @@ class CallScreenPresenter @AssistedInject constructor(
LaunchedEffect(Unit) {
interceptor.interceptedMessages
.onEach {
// We are receiving messages from the WebView, consider that the application is loaded
ignoreWebViewError = true
// Relay message to Widget Driver
callWidgetDriver.value?.send(it)
@ -163,11 +167,18 @@ class CallScreenPresenter @AssistedInject constructor(
is CallScreenEvents.SetupMessageChannels -> {
messageInterceptor.value = event.widgetMessageInterceptor
}
is CallScreenEvents.OnWebViewError -> {
if (!ignoreWebViewError) {
webViewError = event.description.orEmpty()
}
// Else ignore the error, give a chance the Element Call to recover by itself.
}
}
}
return CallScreenState(
urlState = urlState.value,
webViewError = webViewError,
userAgent = userAgent,
isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) },

View file

@ -11,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData
data class CallScreenState(
val urlState: AsyncData<String>,
val webViewError: String?,
val userAgent: String,
val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit,

View file

@ -16,17 +16,20 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
aCallScreenState(),
aCallScreenState(urlState = AsyncData.Loading()),
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
aCallScreenState(webViewError = "Error details from WebView"),
)
}
internal fun aCallScreenState(
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
webViewError: String? = null,
userAgent: String = "",
isInWidgetMode: Boolean = false,
eventSink: (CallScreenEvents) -> Unit = {},
): CallScreenState {
return CallScreenState(
urlState = urlState,
webViewError = webViewError,
userAgent = userAgent,
isInWidgetMode = isInWidgetMode,
eventSink = eventSink,

View file

@ -85,35 +85,48 @@ internal fun CallScreenView(
BackHandler {
handleBack()
}
CallWebView(
modifier = Modifier
if (state.webViewError != null) {
ErrorDialog(
content = buildString {
append(stringResource(CommonStrings.error_unknown))
state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) }
},
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
} else {
CallWebView(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.fillMaxSize(),
url = state.urlState,
userAgent = state.userAgent,
onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
url = state.urlState,
userAgent = state.userAgent,
onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
}
)
when (state.urlState) {
AsyncData.Uninitialized,
is AsyncData.Loading ->
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
is AsyncData.Failure ->
ErrorDialog(
content = state.urlState.error.message.orEmpty(),
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
is AsyncData.Success -> Unit
}
)
when (state.urlState) {
AsyncData.Uninitialized,
is AsyncData.Loading ->
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
is AsyncData.Failure ->
ErrorDialog(
content = state.urlState.error.message.orEmpty(),
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
is AsyncData.Success -> Unit
}
}
}

View file

@ -8,16 +8,23 @@
package io.element.android.features.call.impl.utils
import android.graphics.Bitmap
import android.net.http.SslError
import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import io.element.android.features.call.impl.BuildConfig
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
class WebViewWidgetMessageInterceptor(
private val webView: WebView,
private val onError: (String?) -> Unit,
) : WidgetMessageInterceptor {
companion object {
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this
@ -45,16 +52,35 @@ class WebViewWidgetMessageInterceptor(
if (message.data.response && message.data.api == "toWidget"
|| !message.data.response && message.data.api == "fromWidget") {
let json = JSON.stringify(event.data)
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } }
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG }}
$LISTENER_NAME.postMessage(json);
} else {
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } }
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG }}
}
});
""".trimIndent(),
null
)
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
// No network for instance, transmit the error
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
onError(error?.description?.toString())
super.onReceivedError(view, request, error)
}
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")
onError(errorResponse?.statusCode.toString())
super.onReceivedHttpError(view, request, errorResponse)
}
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
Timber.e("onReceivedSslError error: ${error?.primaryError}")
onError(error?.primaryError?.toString())
super.onReceivedSslError(view, handler, error)
}
}
// Create a WebMessageListener, which will receive messages from the WebView and reply to them

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
</resources>

View file

@ -71,6 +71,7 @@ class CallScreenPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.webViewError).isNull()
assertThat(initialState.isInWidgetMode).isFalse()
analyticsLambda.assertions().isNeverCalled()
joinedCallLambda.assertions().isCalledOnce()
@ -270,6 +271,48 @@ class CallScreenPresenterTest {
assert(stopSyncLambda).isCalledOnce()
}
@Test
fun `present - error from WebView are updating the state`() = runTest {
val presenter = createCallScreenPresenter(
callType = CallType.ExternalUrl("https://call.element.io"),
activeCallManager = FakeActiveCallManager(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Wait until the URL is loaded
skipItems(1)
val initialState = awaitItem()
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
val finalState = awaitItem()
assertThat(finalState.webViewError).isEqualTo("A Webview error")
}
}
@Test
fun `present - error from WebView are ignored if Element Call is loaded`() = runTest {
val presenter = createCallScreenPresenter(
callType = CallType.ExternalUrl("https://call.element.io"),
activeCallManager = FakeActiveCallManager(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Wait until the URL is loaded
skipItems(1)
val initialState = awaitItem()
val messageInterceptor = FakeWidgetMessageInterceptor()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
// Emit a message
messageInterceptor.givenInterceptedMessage("A message")
// WebView emits an error, but it will be ignored
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
val finalState = awaitItem()
assertThat(finalState.webViewError).isNull()
}
}
private fun TestScope.createCallScreenPresenter(
callType: CallType,
navigator: CallScreenNavigator = FakeCallScreenNavigator(),

View file

@ -1,3 +1,6 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
*
@ -7,7 +10,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -21,13 +23,9 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)

View file

@ -9,7 +9,6 @@ package io.element.android.features.createroom.impl.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(CreateRoomScope::class)
@MergeSubcomponent(CreateRoomScope::class)
interface CreateRoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
fun build(): CreateRoomComponent
}

View file

@ -0,0 +1,13 @@
<?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">"اتاق جدید"</string>
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
<string name="screen_create_room_private_option_description">"پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند."</string>
<string name="screen_create_room_private_option_title">"اتاق خصوصی (فقط دعوت)"</string>
<string name="screen_create_room_public_option_description">"پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید."</string>
<string name="screen_create_room_public_option_title">"اتاق عمومی (هرکسی)"</string>
<string name="screen_create_room_room_name_label">"نام اتاق"</string>
<string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
</resources>

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -21,13 +22,9 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)

View file

@ -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_deactivate_account_title">"Дэактываваць уліковы запіс"</string>
</resources>

View file

@ -2,5 +2,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest"</string>
<string name="screen_deactivate_account_delete_all_messages">"Kustuta kõik minu sõnumid"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi."</string>
<string name="screen_deactivate_account_description">"Sinu konto kasutusest eemaldamine on %1$s ja sellega:"</string>
<string name="screen_deactivate_account_description_bold_part">"pöördumatu"</string>
<string name="screen_deactivate_account_list_item_1">"Sinu kasutajakonto %1$s (sa ei saa enam sellega võrku logida ning kasutajatunnust ei saa enam pruukida)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"jäädavalt eemaldatakse kasutusest"</string>
<string name="screen_deactivate_account_list_item_2">"Sind logitakse välja kõikidest jututubadest."</string>
<string name="screen_deactivate_account_list_item_3">"Kustutatakse sinu andmed meie isikutuvastusserverist."</string>
<string name="screen_deactivate_account_list_item_4">"Sinu sõnumid on jätkuvalt nähtavad registreeritud kasutajatele, kuid kui otsustad sõnumid kustutada, siis nad nad pole nähtavad uutele ja registreerimata kasutajatele."</string>
<string name="screen_deactivate_account_title">"Eemalda konto kasutusest"</string>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_delete_all_messages">"حذف همهٔ پیام‌هایم"</string>
<string name="screen_deactivate_account_description_bold_part">"بازگشت‌ناپذیر"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"از کار انداختن دایمی"</string>
<string name="screen_deactivate_account_list_item_2">"برداشتنتان از همهٔ اتاق‌های گپ."</string>
<string name="screen_deactivate_account_list_item_3">"حذف اطّلاعات حسابتان از کارساز هویت."</string>
<string name="screen_deactivate_account_title">"غیرفعّال‌سازی حساب"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Conferma di voler disattivare il tuo account. Questa azione è irreversibile."</string>
<string name="screen_deactivate_account_delete_all_messages">"Elimina tutti i miei messaggi"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Attenzione: gli utenti futuri potrebbero vedere conversazioni incomplete."</string>
<string name="screen_deactivate_account_description">"La disattivazione del tuo account è %1$s , quindi:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversibile"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s il tuo account (non puoi riaccedere e il tuo ID non può essere riutilizzato)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Disattiva permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Ti rimuove da tutte le stanze di chat."</string>
<string name="screen_deactivate_account_list_item_3">"Elimina le informazioni del tuo account dal nostro server di identità."</string>
<string name="screen_deactivate_account_list_item_4">"I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli."</string>
<string name="screen_deactivate_account_title">"Disattivazione dell\'account"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Potwierdź dezaktywacje konta. Tej akcji nie można cofnąć."</string>
<string name="screen_deactivate_account_delete_all_messages">"Usuń wszystkie moje wiadomości"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Ostrzeżenie: Przyszli użytkownicy mogą zobaczyć niekompletne rozmowy."</string>
<string name="screen_deactivate_account_description">"Dezaktywacja konta jest %1$s, zostanie:"</string>
<string name="screen_deactivate_account_description_bold_part">"nieodwracalna"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s twoje konto (nie będziesz mógł się zalogować, a twoje ID przepadnie)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Permanentnie wyłączy"</string>
<string name="screen_deactivate_account_list_item_2">"Usunie Ciebie ze wszystkich pokoi rozmów."</string>
<string name="screen_deactivate_account_list_item_3">"Usunięte wszystkie dane konta z naszego serwera tożsamości."</string>
<string name="screen_deactivate_account_list_item_4">"Twoje wiadomości wciąż będą widoczne dla zarejestrowanych użytkowników, ale nie będą dostępne dla nowych lub niezarejestrowanych użytkowników, jeśli je usuniesz."</string>
<string name="screen_deactivate_account_title">"Dezaktywuj konto"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirme que pretende desativar a sua conta. Esta ação não pode ser desfeita."</string>
<string name="screen_deactivate_account_delete_all_messages">"Eliminar todas as minhas mensagens"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Aviso: futuros usuários podem ver conversas incompletas."</string>
<string name="screen_deactivate_account_description">"A desativação da sua conta é %1$s, irá:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversível"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s sua conta (não pode voltar a iniciar sessão e o seu ID não pode ser reutilizado)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Desativar permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Removê-lo de todas as salas de chat."</string>
<string name="screen_deactivate_account_list_item_3">"Exclua as informações da sua conta do nosso servidor de identidade."</string>
<string name="screen_deactivate_account_list_item_4">"Suas mensagens ainda estarão visíveis para usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."</string>
<string name="screen_deactivate_account_title">"Desativar conta"</string>
</resources>

View file

@ -5,7 +5,10 @@
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornenie: Budúcim používateľom sa môžu zobraziť neúplné konverzácie."</string>
<string name="screen_deactivate_account_description">"Deaktivácia vášho účtu znamená %1$s, že:"</string>
<string name="screen_deactivate_account_description_bold_part">"nezvratný"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s váš účet (nebudete sa môcť znova prihlásiť a vaše ID nebude možné znova použiť)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Natrvalo zakázať"</string>
<string name="screen_deactivate_account_list_item_2">"Odstrániť vás zo všetkých miestností."</string>
<string name="screen_deactivate_account_list_item_3">"Odstrániť informácie o vašom účte z nášho servera totožností."</string>
<string name="screen_deactivate_account_list_item_4">"Vaše správy budú stále viditeľné pre registrovaných používateľov, ale nebudú dostupné pre nových alebo neregistrovaných používateľov, ak sa ich rozhodnete odstrániť."</string>
<string name="screen_deactivate_account_title">"Deaktivovať účet"</string>
</resources>

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.ftue.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.ftue.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)

View file

@ -42,8 +42,8 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun WelcomeView(
applicationName: String,
modifier: Modifier = Modifier,
onContinueClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = onContinueClick)
OnBoardingPage(

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"می‌توانید بعداً تنظیماتتان را تغییر دهید."</string>
<string name="screen_notification_optin_title">"اجازه به آگاهی‌ها و از دست ندادن پیام‌ها"</string>
<string name="screen_welcome_button">"بزن بریم!"</string>
<string name="screen_welcome_subtitle">"چیزهایی که باید بدانید:"</string>
<string name="screen_welcome_title">"به %1$s خوش آمدید!"</string>
</resources>

View file

@ -15,18 +15,19 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -36,8 +37,9 @@ class DefaultFtueServiceTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.Unknown)
}
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val service = createDefaultFtueService(coroutineScope, sessionVerificationService)
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
)
service.state.test {
// Verification state is unknown, we don't display the flow yet
@ -47,9 +49,6 @@ class DefaultFtueServiceTest {
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
}
// Cleanup
coroutineScope.cancel()
}
@Test
@ -58,10 +57,7 @@ class DefaultFtueServiceTest {
val sessionVerificationService = FakeSessionVerificationService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
@ -75,9 +71,6 @@ class DefaultFtueServiceTest {
service.updateState()
assertThat(service.state.value).isEqualTo(FtueState.Complete)
// Cleanup
coroutineScope.cancel()
}
@Test
@ -88,10 +81,7 @@ class DefaultFtueServiceTest {
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
@ -126,20 +116,15 @@ class DefaultFtueServiceTest {
// Final state
null,
)
// Cleanup
coroutineScope.cancel()
}
@Test
fun `if a check for a step is true, start from the next one`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider,
@ -155,14 +140,10 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent()
assertThat(service.getNextStep(null)).isNull()
// Cleanup
coroutineScope.cancel()
}
@Test
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService()
val lockScreenService = FakeLockScreenService()
@ -170,7 +151,6 @@ class DefaultFtueServiceTest {
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.M,
sessionVerificationService = sessionVerificationService,
coroutineScope = coroutineScope,
analyticsService = analyticsService,
lockScreenService = lockScreenService,
)
@ -182,14 +162,10 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent()
assertThat(service.getNextStep(null)).isNull()
// Cleanup
coroutineScope.cancel()
}
@Test
fun `reset do the expected actions S`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val resetAnalyticsLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetAnalyticsLambda
@ -199,7 +175,6 @@ class DefaultFtueServiceTest {
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sdkIntVersion = Build.VERSION_CODES.S,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
@ -211,7 +186,6 @@ class DefaultFtueServiceTest {
@Test
fun `reset do the expected actions TIRAMISU`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val resetLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetLambda
@ -221,7 +195,6 @@ class DefaultFtueServiceTest {
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
@ -232,17 +205,16 @@ class DefaultFtueServiceTest {
.with(value("android.permission.POST_NOTIFICATIONS"))
}
private fun createDefaultFtueService(
coroutineScope: CoroutineScope,
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private fun TestScope.createDefaultFtueService(
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
lockScreenService: LockScreenService = FakeLockScreenService(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
// First version where notification permission is required
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
) = DefaultFtueService(
sessionCoroutineScope = coroutineScope,
sessionCoroutineScope = backgroundScope,
sessionVerificationService = sessionVerificationService,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
analyticsService = analyticsService,

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.invite.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.invite.api)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.core)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_chat_title">"رد دعوت"</string>
<string name="screen_invites_decline_direct_chat_message">"مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_direct_chat_title">"رد گپ"</string>
<string name="screen_invites_empty_list">"بدون دعوت"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) دعوتتان کرد"</string>
</resources>

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -20,13 +21,9 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.joinroom.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)

View file

@ -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_join_room_join_action">"پیوستن به اتاق"</string>
<string name="screen_join_room_knock_action">"در زدن برای پیوستن"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s هنوز از فضاها پشتیبانی نمی‌کند. می‌توانید روی وب به فضاها دسترسی داشته باشید."</string>
<string name="screen_join_room_space_not_supported_title">"فضاها هنوز پشتیبانی نمی‌شوند"</string>
<string name="screen_join_room_subtitle_knock">"زدن روی این دکمه برای آگاه شدن مدیر اتاق. پس از تأیید می‌توانید به گفت‌وگو بپیوندید."</string>
<string name="screen_join_room_subtitle_no_preview">"برای دیدن تاریخچهٔ پیام باید عضو این اتاق باشید."</string>
<string name="screen_join_room_title_knock">"می‌خواهید به اتاق بپیوندید؟"</string>
<string name="screen_join_room_title_no_preview">"پیش‌نمایش موجود نیست"</string>
</resources>

View file

@ -1,16 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Composable
import io.element.android.libraries.architecture.Presenter
interface LeaveRoomPresenter : Presenter<LeaveRoomState> {
@Composable
override fun present(): LeaveRoomState
}

View file

@ -57,9 +57,10 @@ fun aLeaveRoomState(
confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
eventSink: (LeaveRoomEvent) -> Unit = {},
) = LeaveRoomState(
confirmation = confirmation,
progress = progress,
error = error,
eventSink = {},
eventSink = eventSink,
)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
*
@ -7,20 +9,15 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.features.leaveroom.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)

View file

@ -12,16 +12,14 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -30,12 +28,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultLeaveRoomPresenter @Inject constructor(
class LeaveRoomPresenter @Inject constructor(
private val client: MatrixClient,
private val roomMembershipObserver: RoomMembershipObserver,
private val dispatchers: CoroutineDispatchers,
) : LeaveRoomPresenter {
) : Presenter<LeaveRoomState> {
@Composable
override fun present(): LeaveRoomState {
val scope = rememberCoroutineScope()

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.leaveroom.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@Module
interface LeaveRoomModule {
@Binds
fun bindLeaveRoomPresenter(presenter: LeaveRoomPresenter): Presenter<LeaveRoomState>
}

View file

@ -27,13 +27,13 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class DefaultLeaveRoomPresenterTest {
class LeaveRoomPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state hides all dialogs`() = runTest {
val presenter = createDefaultLeaveRoomPresenter()
val presenter = createLeaveRoomPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -46,7 +46,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show generic confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -66,7 +66,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show private room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -86,7 +86,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show last user in room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -106,7 +106,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show DM confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -127,7 +127,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - leaving a room leaves the room`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -151,7 +151,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show error if leave room fails`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -175,7 +175,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - show progress indicator while leaving a room`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -199,7 +199,7 @@ class DefaultLeaveRoomPresenterTest {
@Test
fun `present - hide error hides the error`() = runTest {
val presenter = createDefaultLeaveRoomPresenter(
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
@ -225,10 +225,10 @@ class DefaultLeaveRoomPresenterTest {
}
}
private fun TestScope.createDefaultLeaveRoomPresenter(
private fun TestScope.createLeaveRoomPresenter(
client: MatrixClient = FakeMatrixClient(),
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
): DefaultLeaveRoomPresenter = DefaultLeaveRoomPresenter(
): LeaveRoomPresenter = LeaveRoomPresenter(
client = client,
roomMembershipObserver = roomMembershipObserver,
dispatchers = testCoroutineDispatchers(false),

View file

@ -1,21 +0,0 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.leaveroom.test"
}
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
api(projects.features.leaveroom.api)
}

View file

@ -1,38 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.leaveroom.fake
import androidx.compose.runtime.Composable
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.api.LeaveRoomState
class FakeLeaveRoomPresenter : LeaveRoomPresenter {
val events = mutableListOf<LeaveRoomEvent>()
private fun handleEvent(event: LeaveRoomEvent) {
events += event
}
private var state = LeaveRoomState(
confirmation = LeaveRoomState.Confirmation.Hidden,
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Hidden,
eventSink = ::handleEvent,
)
set(value) {
field = value.copy(eventSink = ::handleEvent)
}
fun givenState(state: LeaveRoomState) {
this.state = state
}
@Composable
override fun present(): LeaveRoomState = state
}

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
*
@ -16,13 +18,9 @@ android {
namespace = "io.element.android.features.licenses.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(libs.serialization.json)
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)

View file

@ -38,8 +38,8 @@ internal fun StaticMapPlaceholder(
contentDescription: String?,
width: Dp,
height: Dp,
modifier: Modifier = Modifier,
onLoadMapClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
contentAlignment = Alignment.Center,

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
@ -19,9 +20,7 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
api(projects.features.location.api)
@ -39,8 +38,6 @@ dependencies {
implementation(libs.accompanist.permission)
implementation(projects.libraries.uiStrings)
implementation(libs.dagger)
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,7 +9,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.lockscreen.impl"
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil()
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.lockscreen.api)
implementation(projects.appconfig)
implementation(projects.libraries.core)

View file

@ -18,10 +18,10 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.impl.LockScreenConfig
import io.element.android.features.lockscreen.impl.R

View file

@ -7,8 +7,10 @@
package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@ -23,6 +25,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = AsyncAction.Loading),
aPinUnlockState(biometricUnlockResult = BiometricUnlock.AuthenticationResult.Failure(
BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled")
)),
)
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"هویت‌سنجی زیستی"</string>
<string name="screen_app_lock_biometric_unlock">"قفل‌گشایی زیست‌سنجی"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"قفل‌گشایی با زیست‌سنجی"</string>
<string name="screen_app_lock_forgot_pin">"فراموشی پین؟"</string>
<string name="screen_app_lock_settings_change_pin">"تغییر کد پین"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"احازه به قفل گشایی زیست‌سنجی"</string>
<string name="screen_app_lock_settings_remove_pin">"برداشتن پین"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"مطمئنید که می‌خواهید پین را بردارید؟"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"برداشتن پین؟"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"اجازه به %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"ترجیح می‌دهم از پین استفاده کنم"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"زمیانتان را ذخیره کرده و از %1$s برای قفل‌گشایی هربارهٔ کاره استفاده کنید"</string>
<string name="screen_app_lock_setup_choose_pin">"گزینش پین"</string>
<string name="screen_app_lock_setup_confirm_pin">"تأیید پین"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"به دلیل امنیتی نمی‌توانید این پین را برگزینید"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"گزینشی پینی متفاوت"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"لطفاً یک پین را دو بار وارد کنید"</string>
<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>
<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>
</resources>

View file

@ -1,3 +1,6 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
* Copyright 2022-2024 New Vector Ltd.
*
@ -7,7 +10,6 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
alias(libs.plugins.kotlin.serialization)
}
@ -22,14 +24,10 @@ android {
}
}
anvil {
generateDaggerFactories.set(true)
}
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies {
implementation(projects.anvilannotations)
implementation(projects.appconfig)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)

View file

@ -14,5 +14,4 @@ data class AccountProvider(
val isPublic: Boolean = false,
val isMatrixOrg: Boolean = false,
val isValid: Boolean = false,
val supportSlidingSync: Boolean = false,
)

View file

@ -15,8 +15,7 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
get() = sequenceOf(
anAccountProvider(),
anAccountProvider().copy(subtitle = null),
anAccountProvider().copy(subtitle = null, title = "no.sliding.sync", supportSlidingSync = false),
anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false, supportSlidingSync = false),
anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false),
anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false),
// Add other state here
)
@ -28,5 +27,4 @@ fun anAccountProvider() = AccountProvider(
isPublic = true,
isMatrixOrg = true,
isValid = true,
supportSlidingSync = true,
)

View file

@ -38,8 +38,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun AccountProviderView(
item: AccountProvider,
modifier: Modifier = Modifier,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.login.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
@Module
interface LoginModule {
@Binds
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
}

View file

@ -9,7 +9,6 @@ package io.element.android.features.login.impl.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(QrCodeLoginScope::class)
@MergeSubcomponent(QrCodeLoginScope::class)
interface QrCodeLoginComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
fun build(): QrCodeLoginComponent
}

View file

@ -12,6 +12,4 @@ data class HomeserverData(
val homeserverUrl: String,
// True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
val isWellknownValid: Boolean,
// True if a wellknown file has been found and is valid and is claiming a sliding sync Url
val supportSlidingSync: Boolean,
)

View file

@ -29,7 +29,7 @@ class HomeserverResolver @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest,
) {
suspend fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
val flowContext = currentCoroutineContext()
val trimmedUserInput = userInput.trim()
if (trimmedUserInput.length < 4) return@flow
@ -46,13 +46,11 @@ class HomeserverResolver @Inject constructor(
}
val isValid = wellKnown?.isValid().orFalse()
if (isValid) {
val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse()
// Emit the list as soon as possible
currentList.add(
HomeserverData(
homeserverUrl = url,
isWellknownValid = true,
supportSlidingSync = supportSlidingSync
)
)
withContext(flowContext) {
@ -68,7 +66,6 @@ class HomeserverResolver @Inject constructor(
HomeserverData(
homeserverUrl = trimmedUserInput,
isWellknownValid = false,
supportSlidingSync = false,
)
)
)

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