Merge branch 'release/0.6.5' into main

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

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
debug: debug:
@ -30,11 +30,11 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
build: build:
@ -38,11 +38,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

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

View file

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

View file

@ -8,8 +8,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
build-apk: build-apk:
@ -33,11 +33,11 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@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' if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

@ -7,8 +7,8 @@ on:
- cron: "0 4 * * *" - cron: "0 4 * * *"
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
nightly: nightly:
@ -17,11 +17,11 @@ jobs:
if: ${{ github.repository == 'element-hq/element-x-android' }} if: ${{ github.repository == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Build and upload Nightly application - name: Build and upload Nightly application
run: | run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES

View file

@ -8,8 +8,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
nightlyReports: nightlyReports:
@ -20,11 +20,11 @@ jobs:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
@ -61,11 +61,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

@ -7,8 +7,8 @@ on:
- cron: "0 4 * * *" - cron: "0 4 * * *"
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
nightly: nightly:
@ -23,11 +23,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Build and upload Nightly application - name: Build and upload Nightly application
run: | run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
checkScript: checkScript:
@ -46,11 +46,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
@ -84,11 +84,11 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
@ -124,11 +124,11 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
@ -168,11 +168,11 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
@ -208,11 +208,11 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
@ -248,11 +248,11 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

@ -7,7 +7,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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: jobs:
record: record:
@ -32,11 +33,11 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
# Add gradle cache, this should speed up the process # Add gradle cache, this should speed up the process
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
@ -48,3 +49,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} 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 # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
gplay: gplay:
@ -19,11 +19,11 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
- name: Create app bundle - name: Create app bundle
@ -55,11 +55,11 @@ jobs:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
- name: Create Enterprise app bundle - name: Create Enterprise app bundle
@ -83,11 +83,11 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
- name: Create APKs - name: Create APKs

View file

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

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache
GROUP: ${{ format('sonar-{0}', github.ref) }} GROUP: ${{ format('sonar-{0}', github.ref) }}
jobs: jobs:
@ -27,11 +27,11 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: 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' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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 -Dsonar.gradle.skipCompile=true CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
tests: tests:
@ -46,24 +46,18 @@ jobs:
- name: Clone submodules - name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit tests for debug variant - name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $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: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
@ -71,7 +65,7 @@ jobs:
with: with:
name: kover-error-report name: kover-error-report
path: | path: |
app/build/reports/kover/verifyGplayDebug.err app/build/reports/kover
- name: ✅ Upload kover report (disabled) - name: ✅ Upload kover report (disabled)
if: always() 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 # Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml .idea/navEditor.xml
.idea/other.xml .idea/other.xml
.idea/runConfigurations.xml
.idea/tasks.xml .idea/tasks.xml
.idea/workspace.xml .idea/workspace.xml
.idea/libraries .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) Changes in Element X v0.6.3 (2024-09-19)
======================================== ========================================

View file

@ -7,7 +7,6 @@
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kapt)
} }
dependencies { dependencies {
@ -16,6 +15,6 @@ dependencies {
implementation(libs.anvil.compiler.utils) implementation(libs.anvil.compiler.utils)
implementation(libs.kotlinpoet) implementation(libs.kotlinpoet)
implementation(libs.dagger) implementation(libs.dagger)
compileOnly(libs.google.autoservice.annotations) implementation(libs.ksp.plugin)
kapt(libs.google.autoservice) 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.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig import com.android.build.gradle.tasks.GenerateBuildConfig
import extension.AssetCopyTask import extension.AssetCopyTask
import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource import extension.GitRevisionValueSource
import extension.allEnterpriseImpl import extension.allEnterpriseImpl
@ -19,14 +20,13 @@ import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import extension.koverDependencies import extension.koverDependencies
import extension.locales import extension.locales
import extension.setupAnvil
import extension.setupKover import extension.setupKover
import java.util.Locale import java.util.Locale
plugins { plugins {
id("io.element.android-compose-application") id("io.element.android-compose-application")
alias(libs.plugins.kotlin.android) 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 // When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId) id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit) alias(libs.plugins.knit)
@ -161,9 +161,6 @@ android {
} }
} }
} }
kotlinOptions {
jvmTarget = "17"
}
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
@ -233,27 +230,34 @@ knit {
} }
} }
setupAnvil(
generateDaggerCode = true,
generateDaggerFactoriesUsingAnvil = false,
componentMergingStrategy = ComponentMergingStrategy.KSP,
)
dependencies { dependencies {
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()
if (isEnterpriseBuild) { if (isEnterpriseBuild) {
allEnterpriseImpl(rootDir, logger) allEnterpriseImpl(project)
implementation(projects.appicon.enterprise) implementation(projects.appicon.enterprise)
} else { } else {
implementation(projects.appicon.element) implementation(projects.appicon.element)
} }
allFeaturesImpl(rootDir, logger) allFeaturesImpl(project)
implementation(projects.features.migration.api) implementation(projects.features.migration.api)
implementation(projects.anvilannotations)
implementation(projects.appnav) implementation(projects.appnav)
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
anvil(projects.anvilcodegen) implementation(projects.services.analytics.compose)
// Comment to not include firebase in the project if (ModulesConfig.pushProvidersConfig.includeFirebase) {
"gplayImplementation"(projects.libraries.pushproviders.firebase) "gplayImplementation"(projects.libraries.pushproviders.firebase)
// Comment to not include unified push in the project }
implementation(projects.libraries.pushproviders.unifiedpush) if (ModulesConfig.pushProvidersConfig.includeUnifiedPush) {
implementation(projects.libraries.pushproviders.unifiedpush)
}
implementation(libs.appyx.core) implementation(libs.appyx.core)
implementation(libs.androidx.splash) implementation(libs.androidx.splash)
@ -272,9 +276,6 @@ dependencies {
implementation(libs.matrix.emojibase.bindings) implementation(libs.matrix.emojibase.bindings)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
@ -297,14 +298,13 @@ tasks.withType<GenerateBuildConfig>().configureEach {
licensee { licensee {
allow("Apache-2.0") allow("Apache-2.0")
allow("MIT") allow("MIT")
allow("GPL-2.0-with-classpath-exception")
allow("BSD-2-Clause") allow("BSD-2-Clause")
allowUrl("https://opensource.org/licenses/MIT") allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html") 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://www.zetetic.net/sqlcipher/license/")
allowUrl("https://jsoup.org/license") allowUrl("https://jsoup.org/license")
allowUrl("https://asm.ow2.io/license.html") 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") 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 android.content.Context
import com.squareup.anvil.annotations.MergeComponent import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
@ -19,7 +18,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@MergeComponent(AppScope::class) @MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings { interface AppComponent : NodeFactoriesBindings {
@Component.Factory @MergeComponent.Factory
interface Factory { interface Factory {
fun create( fun create(
@ApplicationContext @BindsInstance @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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class) @MergeSubcomponent(RoomScope::class)
interface RoomComponent : NodeFactoriesBindings { interface RoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance
fun room(room: MatrixRoom): Builder 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class) @MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings { interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance
fun client(matrixClient: MatrixClient): Builder fun client(matrixClient: MatrixClient): Builder

View file

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

View file

@ -7,6 +7,7 @@
package io.element.android.appconfig package io.element.android.appconfig
object SecureBackupConfig { object LearnMoreConfig {
const val LEARN_MORE_URL: String = "https://element.io/help#encryption5" 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") @file:Suppress("UnstableApiUsage")
import extension.allFeaturesApi import extension.allFeaturesApi
import extension.setupAnvil
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.kapt)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -20,13 +19,10 @@ android {
namespace = "io.element.android.appnav" namespace = "io.element.android.appnav"
} }
dependencies { setupAnvil()
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
allFeaturesApi(rootDir, logger) dependencies {
allFeaturesApi(project)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)

View file

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

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш хатні сервер больш не падтрымлівае стары пратакол. Калі ласка, выйдзіце і ўвайдзіце зноў, каб працягнуць выкарыстанне праграмы."</string>
</resources> </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 app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
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.share.api.ShareService import io.element.android.features.share.api.ShareService
import io.element.android.features.share.test.FakeShareService import io.element.android.features.share.test.FakeShareService
import io.element.android.libraries.matrix.test.FakeSdkMetadata 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.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
@ -44,7 +38,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.crashDetectionState.crashDetected).isFalse() assertThat(initialState.crashDetectionState.crashDetected).isFalse()
} }
@ -61,7 +54,7 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(2) skipItems(1)
lambda.assertions().isCalledOnce() lambda.assertions().isCalledOnce()
} }
} }
@ -76,8 +69,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java) assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java)
val initialErrorState = initialState.errorState as AppErrorState.Error val initialErrorState = initialState.errorState as AppErrorState.Error
@ -93,25 +84,9 @@ class RootPresenterTest {
appErrorService: AppErrorStateService = DefaultAppErrorStateService(), appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
shareService: ShareService = FakeShareService {}, shareService: ShareService = FakeShareService {},
): RootPresenter { ): 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( return RootPresenter(
crashDetectionPresenter = crashDetectionPresenter, crashDetectionPresenter = { aCrashDetectionState() },
rageshakeDetectionPresenter = rageshakeDetectionPresenter, rageshakeDetectionPresenter = { aRageshakeDetectionState() },
appErrorStateService = appErrorService, appErrorStateService = appErrorService,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
shareService = shareService, shareService = shareService,

View file

@ -48,7 +48,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml")) config.from(files("$rootDir/tools/detekt/detekt.yml"))
} }
dependencies { dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.12") detektPlugins("io.nlopez.compose.rules:detekt:0.4.15")
} }
// KtLint // KtLint
@ -71,8 +71,9 @@ allprojects {
// To have XML report for Danger // To have XML report for Danger
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
} }
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
filter { filter {
exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") } exclude { element -> element.file.path.contains(generatedPath) }
} }
} }
// Dependency check // 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. * Copyright 2023, 2024 New Vector Ltd.
* *
@ -7,7 +9,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.analytics.impl" namespace = "io.element.android.features.analytics.impl"
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) 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.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -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.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -104,14 +104,9 @@ private fun AnalyticsOptInHeader(
bold = true, bold = true,
tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK, tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK,
) )
ClickableText( ClickableLinkText(
text = text, annotatedString = text,
onClick = { onClick = { onClickTerms() },
text
.getStringAnnotations(LINK_TAG, it, it)
.firstOrNull()
?.let { _ -> onClickTerms() }
},
modifier = Modifier modifier = Modifier
.padding(8.dp), .padding(8.dp),
style = ElementTheme.typography.fontBodyMdRegular 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.AnalyticsConfig import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState 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.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) class AnalyticsPreferencesPresenter @Inject constructor(
class DefaultAnalyticsPreferencesPresenter @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : AnalyticsPreferencesPresenter { ) : Presenter<AnalyticsPreferencesState> {
@Composable @Composable
override fun present(): AnalyticsPreferencesState { override fun present(): AnalyticsPreferencesState {
val localCoroutineScope = rememberCoroutineScope() 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 @Test
fun `present - initial state available`() = runTest { fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta() aBuildMeta()
) )
@ -41,7 +41,7 @@ class AnalyticsPreferencesPresenterTest {
@Test @Test
fun `present - initial state not available`() = runTest { fun `present - initial state not available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false), FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
aBuildMeta() aBuildMeta()
) )
@ -55,7 +55,7 @@ class AnalyticsPreferencesPresenterTest {
@Test @Test
fun `present - enable and disable`() = runTest { fun `present - enable and disable`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta() aBuildMeta()
) )

View file

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

View file

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

View file

@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
sealed interface CallScreenEvents { sealed interface CallScreenEvents {
data object Hangup : CallScreenEvents data object Hangup : CallScreenEvents
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
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 callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) } val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) } var isJoinedCall by rememberSaveable { mutableStateOf(false) }
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag() val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark" val theme = if (ElementTheme.isLightTheme) "light" else "dark"
DisposableEffect(Unit) { DisposableEffect(Unit) {
@ -125,6 +127,8 @@ class CallScreenPresenter @AssistedInject constructor(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
interceptor.interceptedMessages interceptor.interceptedMessages
.onEach { .onEach {
// We are receiving messages from the WebView, consider that the application is loaded
ignoreWebViewError = true
// Relay message to Widget Driver // Relay message to Widget Driver
callWidgetDriver.value?.send(it) callWidgetDriver.value?.send(it)
@ -163,11 +167,18 @@ class CallScreenPresenter @AssistedInject constructor(
is CallScreenEvents.SetupMessageChannels -> { is CallScreenEvents.SetupMessageChannels -> {
messageInterceptor.value = event.widgetMessageInterceptor 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( return CallScreenState(
urlState = urlState.value, urlState = urlState.value,
webViewError = webViewError,
userAgent = userAgent, userAgent = userAgent,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) }, eventSink = { handleEvents(it) },

View file

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

View file

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

View file

@ -85,35 +85,48 @@ internal fun CallScreenView(
BackHandler { BackHandler {
handleBack() handleBack()
} }
CallWebView( if (state.webViewError != null) {
modifier = Modifier 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) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.fillMaxSize(), .fillMaxSize(),
url = state.urlState, url = state.urlState,
userAgent = state.userAgent, userAgent = state.userAgent,
onPermissionsRequest = { request -> onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources) val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) } val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback) requestPermissions(androidPermissions.toTypedArray(), callback)
}, },
onWebViewCreate = { webView -> onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView) val interceptor = WebViewWidgetMessageInterceptor(
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) webView = webView,
val pipController = WebViewPipController(webView) onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) )
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 package io.element.android.features.call.impl.utils
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.http.SslError
import android.webkit.JavascriptInterface 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.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.webkit.WebViewCompat import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature import androidx.webkit.WebViewFeature
import io.element.android.features.call.impl.BuildConfig import io.element.android.features.call.impl.BuildConfig
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
class WebViewWidgetMessageInterceptor( class WebViewWidgetMessageInterceptor(
private val webView: WebView, private val webView: WebView,
private val onError: (String?) -> Unit,
) : WidgetMessageInterceptor { ) : WidgetMessageInterceptor {
companion object { companion object {
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this // 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" if (message.data.response && message.data.api == "toWidget"
|| !message.data.response && message.data.api == "fromWidget") { || !message.data.response && message.data.api == "fromWidget") {
let json = JSON.stringify(event.data) 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); $LISTENER_NAME.postMessage(json);
} else { } 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(), """.trimIndent(),
null 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 // 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) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.webViewError).isNull()
assertThat(initialState.isInWidgetMode).isFalse() assertThat(initialState.isInWidgetMode).isFalse()
analyticsLambda.assertions().isNeverCalled() analyticsLambda.assertions().isNeverCalled()
joinedCallLambda.assertions().isCalledOnce() joinedCallLambda.assertions().isCalledOnce()
@ -270,6 +271,48 @@ class CallScreenPresenterTest {
assert(stopSyncLambda).isCalledOnce() 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( private fun TestScope.createCallScreenPresenter(
callType: CallType, callType: CallType,
navigator: CallScreenNavigator = FakeCallScreenNavigator(), navigator: CallScreenNavigator = FakeCallScreenNavigator(),

View file

@ -1,3 +1,6 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/* /*
* Copyright 2022-2024 New Vector Ltd. * Copyright 2022-2024 New Vector Ltd.
* *
@ -7,7 +10,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -21,13 +23,9 @@ android {
} }
} }
anvil { setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(CreateRoomScope::class) @SingleIn(CreateRoomScope::class)
@MergeSubcomponent(CreateRoomScope::class) @MergeSubcomponent(CreateRoomScope::class)
interface CreateRoomComponent : NodeFactoriesBindings { interface CreateRoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
fun build(): CreateRoomComponent 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. * Copyright 2024 New Vector Ltd.
* *
@ -7,7 +9,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -21,13 +22,9 @@ android {
} }
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) 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"> <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_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">"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> <string name="screen_deactivate_account_title">"Eemalda konto kasutusest"</string>
</resources> </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_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">"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_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_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_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> <string name="screen_deactivate_account_title">"Deaktivovať účet"</string>
</resources> </resources>

View file

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

View file

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

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.
* *
@ -7,7 +9,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -15,13 +16,9 @@ android {
namespace = "io.element.android.features.invite.impl" namespace = "io.element.android.features.invite.impl"
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.invite.api) api(projects.features.invite.api)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.core) 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. * Copyright 2024 New Vector Ltd.
* *
@ -7,7 +9,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -20,13 +21,9 @@ android {
} }
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.joinroom.api) api(projects.features.joinroom.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) 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, confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden, progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden, error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
eventSink: (LeaveRoomEvent) -> Unit = {},
) = LeaveRoomState( ) = LeaveRoomState(
confirmation = confirmation, confirmation = confirmation,
progress = progress, progress = progress,
error = error, error = error,
eventSink = {}, eventSink = eventSink,
) )

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2022-2024 New Vector Ltd. * Copyright 2022-2024 New Vector Ltd.
* *
@ -7,20 +9,15 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
} }
android { android {
namespace = "io.element.android.features.leaveroom.impl" namespace = "io.element.android.features.leaveroom.impl"
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) 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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.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
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm 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.Generic
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom 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.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.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -30,12 +28,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(SessionScope::class) class LeaveRoomPresenter @Inject constructor(
class DefaultLeaveRoomPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) : LeaveRoomPresenter { ) : Presenter<LeaveRoomState> {
@Composable @Composable
override fun present(): LeaveRoomState { override fun present(): LeaveRoomState {
val scope = rememberCoroutineScope() 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.Rule
import org.junit.Test import org.junit.Test
class DefaultLeaveRoomPresenterTest { class LeaveRoomPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state hides all dialogs`() = runTest { fun `present - initial state hides all dialogs`() = runTest {
val presenter = createDefaultLeaveRoomPresenter() val presenter = createLeaveRoomPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -46,7 +46,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show generic confirmation`() = runTest { fun `present - show generic confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -66,7 +66,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show private room confirmation`() = runTest { fun `present - show private room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -86,7 +86,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show last user in room confirmation`() = runTest { fun `present - show last user in room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -106,7 +106,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show DM confirmation`() = runTest { fun `present - show DM confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -127,7 +127,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - leaving a room leaves the room`() = runTest { fun `present - leaving a room leaves the room`() = runTest {
val roomMembershipObserver = RoomMembershipObserver() val roomMembershipObserver = RoomMembershipObserver()
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -151,7 +151,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show error if leave room fails`() = runTest { fun `present - show error if leave room fails`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -175,7 +175,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show progress indicator while leaving a room`() = runTest { fun `present - show progress indicator while leaving a room`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -199,7 +199,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - hide error hides the error`() = runTest { fun `present - hide error hides the error`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -225,10 +225,10 @@ class DefaultLeaveRoomPresenterTest {
} }
} }
private fun TestScope.createDefaultLeaveRoomPresenter( private fun TestScope.createLeaveRoomPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
): DefaultLeaveRoomPresenter = DefaultLeaveRoomPresenter( ): LeaveRoomPresenter = LeaveRoomPresenter(
client = client, client = client,
roomMembershipObserver = roomMembershipObserver, roomMembershipObserver = roomMembershipObserver,
dispatchers = testCoroutineDispatchers(false), 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. * Copyright 2022-2024 New Vector Ltd.
* *
@ -16,13 +18,9 @@ android {
namespace = "io.element.android.features.licenses.impl" namespace = "io.element.android.features.licenses.impl"
} }
anvil { setupAnvil()
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)

View file

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

View file

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

View file

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

View file

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

View file

@ -7,8 +7,10 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock 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.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -23,6 +25,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showBiometricUnlock = false), aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = AsyncAction.Loading), 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. * Copyright 2022-2024 New Vector Ltd.
* *
@ -7,7 +10,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
} }
@ -22,14 +24,10 @@ android {
} }
} }
anvil { setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
generateDaggerFactories.set(true)
}
dependencies { dependencies {
implementation(projects.anvilannotations)
implementation(projects.appconfig) implementation(projects.appconfig)
anvil(projects.anvilcodegen)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)

View file

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

View file

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

View file

@ -38,8 +38,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun AccountProviderView( fun AccountProviderView(
item: AccountProvider, item: AccountProvider,
modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(QrCodeLoginScope::class) @SingleIn(QrCodeLoginScope::class)
@MergeSubcomponent(QrCodeLoginScope::class) @MergeSubcomponent(QrCodeLoginScope::class)
interface QrCodeLoginComponent : NodeFactoriesBindings { interface QrCodeLoginComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
fun build(): QrCodeLoginComponent fun build(): QrCodeLoginComponent
} }

View file

@ -12,6 +12,4 @@ data class HomeserverData(
val homeserverUrl: String, val homeserverUrl: String,
// True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
val isWellknownValid: Boolean, 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 dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest, 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 flowContext = currentCoroutineContext()
val trimmedUserInput = userInput.trim() val trimmedUserInput = userInput.trim()
if (trimmedUserInput.length < 4) return@flow if (trimmedUserInput.length < 4) return@flow
@ -46,13 +46,11 @@ class HomeserverResolver @Inject constructor(
} }
val isValid = wellKnown?.isValid().orFalse() val isValid = wellKnown?.isValid().orFalse()
if (isValid) { if (isValid) {
val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse()
// Emit the list as soon as possible // Emit the list as soon as possible
currentList.add( currentList.add(
HomeserverData( HomeserverData(
homeserverUrl = url, homeserverUrl = url,
isWellknownValid = true, isWellknownValid = true,
supportSlidingSync = supportSlidingSync
) )
) )
withContext(flowContext) { withContext(flowContext) {
@ -68,7 +66,6 @@ class HomeserverResolver @Inject constructor(
HomeserverData( HomeserverData(
homeserverUrl = trimmedUserInput, homeserverUrl = trimmedUserInput,
isWellknownValid = false, isWellknownValid = false,
supportSlidingSync = false,
) )
) )
) )

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