This commit is contained in:
bxdxnn
2026-08-24 20:09:11 +00:00
1876 changed files with 20567 additions and 8738 deletions
-1
View File
@@ -7,7 +7,6 @@
"PR-Dependencies", "PR-Dependencies",
], ],
"ignoreDeps": [ "ignoreDeps": [
"string:app_name",
"gradle", "gradle",
], ],
"minimumReleaseAge": "7 days", "minimumReleaseAge": "7 days",
+2 -2
View File
@@ -51,12 +51,12 @@ jobs:
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 }}
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs - name: Assemble debug APKs
+2 -2
View File
@@ -56,12 +56,12 @@ jobs:
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK - name: Assemble debug Gplay Enterprise APK
+2 -2
View File
@@ -19,12 +19,12 @@ jobs:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
name: Use JDK 21 name: Use JDK 21
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
+2 -2
View File
@@ -44,13 +44,13 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }} ref: ${{ github.ref }}
persist-credentials: false persist-credentials: false
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
name: Use JDK 21 name: Use JDK 21
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK - name: Assemble debug APK
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
+4 -4
View File
@@ -37,13 +37,13 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: false cache-read-only: false
@@ -80,12 +80,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis - name: Dependency analysis
+13 -13
View File
@@ -69,12 +69,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
@@ -108,12 +108,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests - name: Run Konsist tests
@@ -149,12 +149,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests - name: Run compose tests
@@ -183,12 +183,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug - name: Build Gplay Debug
@@ -228,12 +228,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt - name: Run Detekt
@@ -269,12 +269,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check - name: Run Ktlint check
@@ -336,7 +336,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
upload_reports: upload_reports:
name: Project Check Suite name: Project Check Suite
+2 -2
View File
@@ -54,13 +54,13 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' 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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots
+6 -6
View File
@@ -38,12 +38,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Create app bundle - name: Create app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -82,12 +82,12 @@ jobs:
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Create Enterprise app bundle - name: Create Enterprise app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -126,12 +126,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Create APKs - name: Create APKs
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -78,6 +78,10 @@ else
git config --local user.name "${INPUT_AUTHOR_EMAIL}" git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi fi
git add -A git add -A
if git diff --cached --quiet; then
echo "Screenshots are already up to date, nothing to commit."
exit 0
fi
git commit -m "Update screenshots" git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git" GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
+2 -2
View File
@@ -45,12 +45,12 @@ jobs:
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 }}
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures - name: Build debug code and test fixtures
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
+2 -2
View File
@@ -63,12 +63,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
-3
View File
@@ -31,9 +31,6 @@ __pycache__/
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files # Log Files
*.log *.log
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<copyright> <copyright>
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;Element Creations Ltd, Element Software SARL, Element Software Inc.,&#10;and Element Software GmbH (the &quot;Element Group&quot;) only make this file available&#10;under a proprietary license model.&#10;&#10;Without a proprietary license with us, you cannot use this file. The terms of&#10;the proprietary license agreement between you and any member of the Element Group&#10;shall always apply to your use of this file. Unauthorised use, copying, distribution,&#10;or modification of this file, via any medium, is strictly prohibited.&#10;&#10;For details about the licensing terms, you must either visit our website or contact&#10;a member of our sales team." /> <option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;SPDX-License-Identifier: LicenseRef-Element-Commercial&#10;SPDX-License-Identifier: LicenseRef-Element-Proprietary" />
<option name="myName" value="Element Enterprise" /> <option name="myName" value="Element Enterprise" />
</copyright> </copyright>
</component> </component>
-21
View File
@@ -1,21 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "matrix.org"
- tapOn:
id: "login-change_server"
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
- hideKeyboard
- extendedWaitUntil:
visible: "element.io"
timeout: 10000
- tapOn: "element.io"
# Revert to matrix.org
- tapOn:
id: "login-change_server"
- tapOn: "matrix.org"
+1 -2
View File
@@ -3,8 +3,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Sign in manually" - tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml - runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn - takeScreenshot: build/maestro/100-SignIn
- runFlow: changeServer.yaml # The account provider field defaults to matrix.org, so continue straight to the MAS login.
- runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn: - tapOn:
id: "login-continue" id: "login-continue"
## MAS page ## MAS page
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID} appId: ${MAESTRO_APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "Change account provider" visible: "Enter your account provider"
timeout: 10000 timeout: 10000
+3 -2
View File
@@ -37,8 +37,9 @@ PRs must meet these rules.
- Create previews for **all main states** of a Composable. - Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency. - Use `@PreviewsDayNight` for consistency.
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states. - Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states.
- Wrap previews in `ElementPreview { ... }`. - Wrap previews in `ElementPreview { ... }`.
- When writing tests, never try to record the screenshots, the CI will do it.
--- ---
@@ -88,7 +89,7 @@ We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecul
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. | | `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. | | `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. | | `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. | | `FooStatePreviewParam.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. | | `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
--- ---
+175
View File
@@ -1,3 +1,178 @@
Changes in Element X v26.08.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
**Note: this release in an internal release, it will not be pushed to the stores.**
## What's Changed
### ✨ Features
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
### 🙌 Improvements
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
### 🐛 Bugfixes
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
### 🧱 Build
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
### 📄 Documentation
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
### 🚧 In development 🚧
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
### Dependency upgrades
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
### 💼 Element Pro
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
### Others
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
## New Contributors
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
Changes in Element X v26.08.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
## What's Changed
### 🙌 Improvements
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
### 🐛 Bugfixes
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
### 🧱 Build
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
### 🚧 In development 🚧
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
### Dependency upgrades
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
### 💼 Element Pro
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
### Others
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
## New Contributors
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
Changes in Element X v26.08.0 Changes in Element X v26.08.0
============================= =============================
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph. * Adds Node to the specified component graph.
* Equivalent to the following declaration: * Equivalent to the following declaration:
* *
* ```
* @BindingContainer * @BindingContainer
* @ContributesTo(Scope::class) * @ContributesTo(Scope::class)
* abstract class YourNodeModule { * interface YourNodeBindingContainer {
* @Binds
* @Binds * @IntoMap
* @IntoMap * @NodeKey(YourNode::class)
* @NodeKey(YourNode::class) * fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*> * }
*} * ```
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class ContributesNode( annotation class ContributesNode(
+28 -17
View File
@@ -127,24 +127,11 @@ android {
optimization { optimization {
enable = true enable = true
keepRules { keepRules {
files.add(File(projectDir, "common-proguard-rules.pro")) // Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
files.add(getDefaultProguardFile("proguard-android-optimize.txt")) includeDefault = true
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardFile = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
File(projectDir, "enterprise-proguard-rules.pro")
} else {
// These default rules prevent the OSS app from being obfuscated
File(projectDir, "default-proguard-rules.pro")
}
if (flavorProguardFile.exists()) {
files.add(flavorProguardFile)
} else {
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
}
} }
// Our custom keep rules are registered as `keepRules` source folders in the `androidComponents` block below,
// as the former `keepRules.files` DSL is deprecated since AGP 9.
} }
} }
@@ -229,6 +216,30 @@ androidComponents {
) )
onVariants { variant -> onVariants { variant ->
// Register the R8 keep rules source folders for optimized build types (release, nightly).
// Replaces the deprecated `optimization.keepRules.files` DSL (AGP 9+).
if (variant.buildType != "debug") {
variant.sources.keepRules?.let { keepRules ->
// Common rules, always applied.
keepRules.addStaticSourceDirectory("proguard/common")
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardDir = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
"../enterprise/proguard"
} else {
// Custom fules for FOSS builds
"proguard/foss"
}
if (File(projectDir, flavorProguardDir).exists()) {
keepRules.addStaticSourceDirectory(flavorProguardDir)
} else {
logger.warn("Proguard folder ${File(projectDir, flavorProguardDir).absolutePath} does not exist")
}
}
}
// Assigns a different version code for each output APK // Assigns a different version code for each output APK
// other than the universal APK. // other than the universal APK.
variant.outputs.forEach { output -> variant.outputs.forEach { output ->
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@@ -38,7 +38,7 @@ import java.io.File
@BindingContainer @BindingContainer
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
object AppModule { object AppBindingContainer {
@Provides @Provides
@BaseDirectory @BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File { fun providesBaseDirectory(@ApplicationContext context: Context): File {
@@ -74,17 +74,18 @@ object AppModule {
fun providesBuildMeta( fun providesBuildMeta(
@ApplicationContext context: Context, @ApplicationContext context: Context,
buildType: BuildType, buildType: BuildType,
enterpriseService: EnterpriseService, isEnterpriseBuild: IsEnterpriseBuild,
): BuildMeta { ): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name) val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
val isEnterprise = isEnterpriseBuild()
return BuildMeta( return BuildMeta(
isDebuggable = BuildConfig.DEBUG, isDebuggable = BuildConfig.DEBUG,
buildType = buildType, buildType = buildType,
applicationName = applicationName, applicationName = applicationName,
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME, productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME, desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID, applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild, isEnterpriseBuild = isEnterprise,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false, lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,
@@ -110,7 +111,7 @@ object AppModule {
@Provides @Provides
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
fun provideSnackbarDispatcher(): SnackbarDispatcher { fun providesSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher() return SnackbarDispatcher()
} }
} }
@@ -1,16 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
object PushConfig {
/**
* Note: pusher_app_id cannot exceed 64 chars.
*/
const val PUSHER_APP_ID: String = "im.vector.app.android"
}
@@ -35,4 +35,11 @@ object RageshakeConfig {
* The maximum number of log lines a rageshake can contain. * The maximum number of log lines a rageshake can contain.
*/ */
const val MAX_LOG_LINES_SIZE = 1_000_000 const val MAX_LOG_LINES_SIZE = 1_000_000
/**
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
* mainly the list of the omitted log file names.
*/
const val MAX_DESCRIPTION_SIZE = 40 * 1024
} }
+8
View File
@@ -44,6 +44,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.wellknown.api)
implementation(projects.features.login.api) implementation(projects.features.login.api)
implementation(libs.coil) implementation(libs.coil)
@@ -60,16 +61,23 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test) testImplementation(projects.features.share.test)
testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test) testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test) testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test) testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
@@ -11,8 +11,10 @@ package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -49,11 +51,13 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.location.api.LocalMapTilerConfig
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -153,6 +157,7 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>( ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Placeholder, initialElement = NavTarget.Placeholder,
@@ -330,11 +335,11 @@ class LoggedInFlowNode(
} }
NavTarget.Home -> { NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback { val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) { override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
lifecycleScope.launch { lifecycleScope.launch {
attachRoom( attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(), roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom), initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
clearBackstack = false, clearBackstack = false,
) )
} }
@@ -677,6 +682,12 @@ class LoggedInFlowNode(
val colors by remember { val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId) enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default) }.collectAsState(SemanticColorsLightDark.default)
val currentMapTilerConfig = LocalMapTilerConfig.current
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
}
ElementThemeApp( ElementThemeApp(
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
@@ -684,16 +695,18 @@ class LoggedInFlowNode(
compoundDark = colors.dark, compoundDark = colors.dark,
buildMeta = buildMeta, buildMeta = buildMeta,
) { ) {
val isOnline by syncService.isOnline.collectAsState() CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) {
ConnectivityIndicatorContainer( val isOnline by syncService.isOnline.collectAsState()
isOnline = isOnline, ConnectivityIndicatorContainer(
modifier = modifier, isOnline = isOnline,
) { contentModifier -> modifier = modifier,
Box(modifier = contentModifier) { ) { contentModifier ->
val ftueState by ftueService.state.collectAsState() Box(modifier = contentModifier) {
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack)) val ftueState by ftueService.state.collectAsState()
if (ftueState is FtueState.Complete) { BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
} }
} }
} }
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomFlowNode
@@ -40,6 +39,7 @@ import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView import io.element.android.appnav.root.RootView
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
@@ -109,6 +109,13 @@ class RootFlowNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
/**
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
* Kept here so that the root nav target can be computed from both the logged in state and the pending
* login params, whatever the order in which the intent and the first nav state emission are processed.
*/
private var pendingLoginParams: LoginParams? = null
override fun onBuilt() { override fun onBuilt() {
analyticsColdStartWatcher.start() analyticsColdStartWatcher.start()
appCoroutineScope.launch { appCoroutineScope.launch {
@@ -152,7 +159,7 @@ class RootFlowNode(
} }
} }
LoggedInState.NotLoggedIn -> { LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null) switchToNotLoggedInFlow(pendingLoginParams)
} }
} }
} }
@@ -194,15 +201,18 @@ class RootFlowNode(
} }
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
pendingLoginParams = null
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
} }
private fun switchToNotLoggedInFlow(params: LoginParams?) { private fun switchToNotLoggedInFlow(params: LoginParams?) {
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll() matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
} }
private fun switchToSignedOutFlow(sessionId: SessionId) { private fun switchToSignedOutFlow(sessionId: SessionId) {
pendingLoginParams = null
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId)) backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
} }
@@ -319,6 +329,7 @@ class RootFlowNode(
} }
override fun onDone() { override fun onDone() {
pendingLoginParams = null
backstack.pop() backstack.pop()
} }
} }
@@ -418,6 +429,7 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled") Timber.w("Login link ignored, multi account is disabled")
} }
} else { } else {
pendingLoginParams = params
switchToNotLoggedInFlow(params) switchToNotLoggedInFlow(params)
} }
} else { } else {
@@ -76,6 +76,7 @@ class IntentResolver(
} }
private fun Intent.canBeIgnored(): Boolean { private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN && return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true categories?.contains(Intent.CATEGORY_LAUNCHER) == true
} }
@@ -120,6 +120,12 @@ class LoggedInPresenter(
}.launchIn(this) }.launchIn(this)
} }
LaunchedEffect(Unit) {
// Keep automatic call status (m.call) in sync with homeserver support.
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
matrixClient.enableAutomaticCallStatus(enabled)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState() val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) { LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) { if (networkConnectivity == NetworkStatus.Connected) {
@@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> { open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
get() = sequenceOf( get() = sequenceOf(
aLoggedInState(), aLoggedInState(),
@@ -124,7 +124,7 @@ private fun ForceNativeSlidingSyncMigrationDialog(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview { internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStatePreviewParam::class) state: LoggedInState) = ElementPreview {
LoggedInView( LoggedInView(
state = state, state = state,
navigateToNotificationTroubleshoot = {}, navigateToNotificationTroubleshoot = {},
@@ -29,7 +29,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.DelayedVisibility import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider import io.element.android.libraries.matrix.ui.room.LoadingRoomStatePreviewParam
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@@ -84,7 +84,7 @@ private fun LoadingRoomTopBar(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview { internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStatePreviewParam::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView( LoadingRoomNodeView(
state = state, state = state,
onBackClick = {} onBackClick = {}
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.core.state.SavedStateMap
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.MatrixSessionCache import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.preferences.api.CacheService import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
@@ -14,7 +14,7 @@ import io.element.android.features.rageshake.api.detection.aRageshakeDetectionSt
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.aAppErrorState import io.element.android.services.apperror.api.aAppErrorState
open class RootStateProvider : PreviewParameterProvider<RootState> { open class RootStatePreviewParam : PreviewParameterProvider<RootState> {
override val values: Sequence<RootState> override val values: Sequence<RootState>
get() = sequenceOf( get() = sequenceOf(
aRootState().copy( aRootState().copy(
@@ -60,7 +60,7 @@ fun RootView(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview { internal fun RootViewPreview(@PreviewParameter(RootStatePreviewParam::class) rootState: RootState) = ElementPreview {
RootView( RootView(
state = rootState, state = rootState,
onOpenBugReport = {}, onOpenBugReport = {},
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.appnav.di package io.element.android.appnav.session
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
@@ -115,7 +115,7 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) { private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create( val syncOrchestrator = syncOrchestratorFactory.create(
syncService = matrixClient.syncService, matrixClient = matrixClient,
sessionCoroutineScope = matrixClient.sessionCoroutineScope, sessionCoroutineScope = matrixClient.sessionCoroutineScope,
) )
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.appnav.di package io.element.android.appnav.session
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction import io.element.android.services.analytics.api.recordTransaction
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
@AssistedInject @AssistedInject
class SyncOrchestrator( class SyncOrchestrator(
@Assisted private val syncService: SyncService, @Assisted private val matrixClient: MatrixClient,
@Assisted sessionCoroutineScope: CoroutineScope, @Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
@@ -47,13 +47,15 @@ class SyncOrchestrator(
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create( fun create(
syncService: SyncService, matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator ): SyncOrchestrator
} }
private val tag = "SyncOrchestrator" private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag) private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false) private val started = AtomicBoolean(false)
@@ -94,7 +96,7 @@ class SyncOrchestrator(
appForegroundStateService.isInCall, appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent, appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall, appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation appForegroundStateService.isSharingLiveLocation,
) )
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } } val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine( combine(
@@ -103,6 +105,11 @@ class SyncOrchestrator(
networkMonitor.connectivity, networkMonitor.connectivity,
isAppActiveFlow, isAppActiveFlow,
) { syncState, networkState, isAppActive -> ) { syncState, networkState, isAppActive ->
if (matrixClient.isShuttingDown) {
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
return@combine SyncStateAction.NoOp
}
val isNetworkAvailable = networkState == NetworkStatus.Connected val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable") Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"تسجيل الخروج والترقية"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"لم يعد %1$s يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"لم يعد خادمك المنزلي يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
</resources>
@@ -2,4 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string> <string name="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"‏%1$s دیگر از شیوه‌نامهٔ قدیمی پشتیبانی نمی‌کند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."</string> <string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"‏%1$s دیگر از شیوه‌نامهٔ قدیمی پشتیبانی نمی‌کند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"سرور خانگی شما دیگر از پروتکل قدیمی پشتیبانی نمی‌کند. لطفاً برای ادامه استفاده از برنامه، از سیستم خارج شده و دوباره وارد شوید."</string>
</resources> </resources>
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.appnav
import android.content.Intent
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.AncestryInfo
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginEntryPoint
import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
import io.element.android.features.preferences.test.FakeCacheService
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.features.share.test.FakeShareIntentHandler
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
import io.element.android.libraries.architecture.AssistedNodeFactory
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
import io.element.android.services.apperror.test.FakeAppErrorStateService
import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings
import io.element.android.tests.testutils.node.FakeParentNode
import io.element.android.tests.testutils.presenter.NotUsedPresenter
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
private const val A_LOGIN_LINK = "https://mobile.element.io/element/?account_provider=example.com&login_hint=mxid:@alice:example.com"
private val A_LOGIN_PARAMS = LoginParams(
accountProvider = "example.com",
loginHint = "mxid:@alice:example.com",
)
private val A_LOGIN_ENTRY_POINT_PARAMS = LoginEntryPoint.Params(
accountProvider = A_LOGIN_PARAMS.accountProvider,
loginHint = A_LOGIN_PARAMS.loginHint,
)
class RootFlowNodeTest : RobolectricTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest {
var loginEntryPointParams: LoginEntryPoint.Params? = null
val rootFlowNode = createRootFlowNode(
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
loginEntryPointParams = params
node(buildContext) {}
},
)
rootFlowNode.parentNodeTestHelper()
// The intent is handled first, this is what happens when the deep link cold starts the app.
rootFlowNode.handleIntent(aLoginIntent())
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
// Then the first nav state emission lands, it must not override the login params.
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
}
@Test
fun `given no session, when a login link is handled after the nav state is observed, then the login params are applied`() = runTest {
var loginEntryPointParams: LoginEntryPoint.Params? = null
val rootFlowNode = createRootFlowNode(
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
loginEntryPointParams = params
node(buildContext) {}
},
)
rootFlowNode.parentNodeTestHelper()
// The nav state is observed first, this is what happens when the app is already running.
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(null))
rootFlowNode.handleIntent(aLoginIntent())
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
}
private fun aLoginIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(A_LOGIN_LINK))
private fun TestScope.createRootFlowNode(
loginEntryPoint: LoginEntryPoint,
sessionStore: SessionStore = InMemorySessionStore(),
): RootFlowNode {
val matrixSessionCache = MatrixSessionCache(
authenticationService = FakeMatrixAuthenticationService(),
syncOrchestratorFactory = FakeSyncOrchestratorFactory(),
analyticsService = FakeAnalyticsService(),
)
val parentNode = FakeParentNode(
graph = FakeNodeFactoriesBindings(
mapOf(
NotLoggedInFlowNode::class to AssistedNodeFactory { buildContext, plugins ->
NotLoggedInFlowNode(
buildContext = buildContext,
plugins = plugins,
loginEntryPoint = loginEntryPoint,
imageLoaderHolder = FakeImageLoaderHolder(),
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
)
}
)
)
)
return RootFlowNode(
buildContext = BuildContext(
ancestryInfo = AncestryInfo.Child(anchor = parentNode),
savedStateMap = null,
customisations = NodeCustomisationDirectoryImpl(),
),
plugins = emptyList(),
sessionStore = sessionStore,
accountProviderAccessControl = FakeAccountProviderAccessControl { true },
navStateFlowFactory = RootNavStateFlowFactory(
sessionStore = sessionStore,
cacheService = FakeCacheService(),
matrixSessionCache = matrixSessionCache,
imageLoaderHolder = FakeImageLoaderHolder(),
sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
),
matrixSessionCache = matrixSessionCache,
presenter = RootPresenter(
crashDetectionPresenter = NotUsedPresenter(),
rageshakeDetectionPresenter = NotUsedPresenter(),
appErrorStateService = FakeAppErrorStateService(),
analyticsService = FakeAnalyticsService(),
sdkMetadata = FakeSdkMetadata("sha"),
),
bugReportEntryPoint = FakeBugReportEntryPoint(),
signedOutEntryPoint = FakeSignedOutEntryPoint(),
accountSelectEntryPoint = FakeAccountSelectEntryPoint(),
intentResolver = IntentResolver(
deeplinkParser = { null },
loginIntentResolver = FakeLoginIntentResolver { A_LOGIN_PARAMS },
oAuthIntentResolver = FakeOAuthIntentResolver { null },
permalinkParser = FakePermalinkParser(),
shareIntentHandler = FakeShareIntentHandler(),
),
oAuthActionFlow = FakeOAuthActionFlow(),
featureFlagService = FakeFeatureFlagService(),
announcementService = FakeAnnouncementService(),
analyticsService = FakeAnalyticsService(),
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
appCoroutineScope = backgroundScope,
)
}
}
@@ -8,10 +8,11 @@
package io.element.android.appnav package io.element.android.appnav
import io.element.android.appnav.di.SyncOrchestrator import io.element.android.appnav.session.SyncOrchestrator
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
} }
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected) val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
) )
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
} }
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
) )
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true) val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true) val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false, initialIsSyncingNotificationEventValue = false,
) )
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false, initialIsSyncingNotificationEventValue = false,
) )
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
initialHasRingingCall = false, initialHasRingingCall = false,
) )
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = true, initialIsInCallValue = true,
) )
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = false, initialIsInCallValue = false,
) )
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
) )
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
} }
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected) val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator( val syncOrchestrator = createSyncOrchestrator(
syncService = syncService, matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
) )
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
startSyncRecorder.assertions().isNeverCalled() startSyncRecorder.assertions().isNeverCalled()
} }
@Test
fun `when the client is shutting down, the orchestrator does nothing`() = runTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false,
initialIsSyncingNotificationEventValue = false,
initialIsInCallValue = false,
)
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val syncOrchestrator = createSyncOrchestrator(
matrixClient = FakeMatrixClient(syncService = syncService, isShuttingDownResult = { true }),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// These should still not trigger a sync, sync the client is shutting down
networkMonitor.givenNetworkBlocked(false)
appForegroundStateService.updateIsInCallState(true)
appForegroundStateService.isInForeground.value = true
advanceTimeBy(10.seconds)
startSyncRecorder.assertions().isNeverCalled()
}
private fun TestScope.createSyncOrchestrator( private fun TestScope.createSyncOrchestrator(
syncService: FakeSyncService = FakeSyncService(), matrixClient: FakeMatrixClient = FakeMatrixClient(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(), networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(), appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator( ) = SyncOrchestrator(
syncService = syncService, matrixClient = matrixClient,
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
assertThat(result).isNull() assertThat(result).isNull()
} }
@Test
fun `resolve intent replayed from the history should return null`() {
val sut = createIntentResolver(
deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
eventId = null,
)
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
@Test @Test
fun `test resolve navigation intent root`() { fun `test resolve navigation intent root`() {
val sut = createIntentResolver( val sut = createIntentResolver(
@@ -48,6 +48,7 @@ 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.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
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
@@ -346,6 +347,44 @@ class LoggedInPresenterTest {
} }
} }
@Test
fun `present - does not enable automatic call status when server does not support it`() = runTest {
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
).apply {
isUserStatusSupportedResult = Result.success(false)
}
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
.isCalledOnce()
.with(value(false))
}
}
@Test
fun `present - enables automatic call status when server supports it`() = runTest {
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
).apply {
isUserStatusSupportedResult = Result.success(true)
}
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
.isCalledOnce()
.with(value(true))
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1) skipItems(1)
return awaitItem() return awaitItem()
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.CoroutineScope
class FakeSyncOrchestratorFactory : SyncOrchestrator.Factory {
override fun create(
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator = lambdaError()
}
@@ -6,12 +6,12 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.appnav.di package io.element.android.appnav.session
import com.bumble.appyx.core.state.MutableSavedStateMapImpl import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
return object : SyncOrchestrator.Factory { return object : SyncOrchestrator.Factory {
override fun create( override fun create(
syncService: SyncService, matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator { ): SyncOrchestrator {
return SyncOrchestrator( return SyncOrchestrator(
syncService = syncService, matrixClient = matrixClient,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(), appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(), networkMonitor = FakeNetworkMonitor(),
+1 -1
View File
@@ -98,7 +98,7 @@ allprojects {
// This is disabled by default, but the CI will enforce this. // This is disabled by default, but the CI will enforce this.
// You can override by passing `-PallWarningsAsErrors=true` in the command line // You can override by passing `-PallWarningsAsErrors=true` in the command line
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file // Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true" allWarningsAsErrors = findProperty("allWarningsAsErrors") == "true"
// Uncomment to suppress Compose Kotlin compiler compatibility warning // Uncomment to suppress Compose Kotlin compiler compatibility warning
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true")) // freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
+8
View File
@@ -1,3 +1,5 @@
import org.sonarqube.gradle.SonarResolverTask
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd. * Copyright 2022-2025 New Vector Ltd.
@@ -18,3 +20,9 @@ dependencies {
implementation(libs.ksp.plugin) implementation(libs.ksp.plugin)
implementation(libs.kotlinpoet.ksp) implementation(libs.kotlinpoet.ksp)
} }
// Ensure that the processResources task is executed before the SonarResolverTask to avoid issues with missing resources during SonarQube analysis.
// This *should not be necesssary* but is a workaround for a known issue with the SonarQube Gradle plugin.
tasks.withType<SonarResolverTask>().configureEach {
dependsOn("processResources")
}
@@ -61,32 +61,32 @@ class ContributesNodeProcessor(
if (config.enableLogging) { if (config.enableLogging) {
logger.warn("Processing ${ksClass.qualifiedName?.asString()}") logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
} }
generateModule(ksClass) generateBindingContainer(ksClass)
generateFactory(ksClass) generateFactory(ksClass)
} }
return invalidSymbols return invalidSymbols
} }
private fun generateModule(ksClass: KSClassDeclaration) { private fun generateBindingContainer(ksClass: KSClassDeclaration) {
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!! val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
val modulePackage = ksClass.packageName.asString() val bindingContainerPackage = ksClass.packageName.asString()
val moduleClassName = "${ksClass.simpleName.asString()}_Module" val bindingContainerClassName = "${ksClass.simpleName.asString()}BindingContainer"
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString()) val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val content = FileSpec.builder( val content = FileSpec.builder(
packageName = modulePackage, packageName = bindingContainerPackage,
fileName = moduleClassName, fileName = bindingContainerClassName,
) )
.addType( .addType(
TypeSpec.interfaceBuilder(moduleClassName) TypeSpec.interfaceBuilder(bindingContainerClassName)
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build()) .addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
.addAnnotation(BindingContainer::class) .addAnnotation(BindingContainer::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build()) .addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
.addFunction( .addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory") FunSpec.builder("bind${ksClass.simpleName.asString()}AssistedFactory")
.addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory")) .addParameter("factory", ClassName(bindingContainerPackage, "${ksClass.simpleName.asString()}AssistedFactory"))
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR)) .returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
.addAnnotation(Binds::class) .addAnnotation(Binds::class)
.addAnnotation(IntoMap::class) .addAnnotation(IntoMap::class)
@@ -114,7 +114,7 @@ class ContributesNodeProcessor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
private fun generateFactory(ksClass: KSClassDeclaration) { private fun generateFactory(ksClass: KSClassDeclaration) {
val generatedPackage = ksClass.packageName.asString() val generatedPackage = ksClass.packageName.asString()
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory" val assistedFactoryClassName = "${ksClass.simpleName.asString()}AssistedFactory"
val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() } val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) } val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
if (assistedParameters.size != 2) { if (assistedParameters.size != 2) {
+9 -5
View File
@@ -303,6 +303,8 @@ Follow these steps to install and configure the plugin and templates:
- Click on OK - Click on OK
4. Configure generate-module-from-template plugin : 4. Configure generate-module-from-template plugin :
- Navigate to AS/Settings/Tools/Module Template Settings - Navigate to AS/Settings/Tools/Module Template Settings
- If a `FeatureModule` template is already listed, select it and remove it, since the plugin keeps its own copy of the
imported template and will not pick up changes made to `FeatureModule.json` on its own
- Click on + / Import From File - Click on + / Import From File
- Pick the `tools/templates/FeatureModule.json` - Pick the `tools/templates/FeatureModule.json`
@@ -320,9 +322,11 @@ Example for a new feature called RoomDetails:
4. Verify that the structure looks ok and click on Finish 4. Verify that the structure looks ok and click on Finish
5. The modules api/impl should be created under `features/roomdetails` directory. 5. The modules api/impl should be created under `features/roomdetails` directory.
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle). 6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`. 7. You can now add more Presentation classes (Events, State, StatePreviewParam, View, Presenter) in the impl module with the `Template Presentation Classes`.
To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`. To use it, just right click on the package where you want to generate classes, and click on `New` / `Template Presentation Classes`.
Fill the text field with the base name of the classes, ie `RootRoomDetails` in the `root` package. Fill the text field with the base name of the classes: the generated files are `<BaseName>Event.kt`, `<BaseName>State.kt`,
`<BaseName>StatePreviewParam.kt`, `<BaseName>Presenter.kt`, `<BaseName>Node.kt` and `<BaseName>View.kt`. For instance
`PreferencesRoot` in the `features/preferences/impl/root` package.
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
@@ -356,9 +360,9 @@ We have 3 tests frameworks in place, and this should be sufficient to guarantee
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md). - Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test, - Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewProvider to provide file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewParam classes to provide
different states. See for instance the different states. See for instance the
file [TemplateStateProvider.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt) file [TemplateStatePreviewParam.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStatePreviewParam.kt)
- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt). - Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake **Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
-2
View File
@@ -6,8 +6,6 @@ Rust sdk branch: https://github.com/matrix-org/matrix-rust-sdk/tree/oidc-ffi
Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0 Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
Server list: https://github.com/element-hq/oidc-playground
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28) Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
clientName: InfoPlistReader.main.bundleDisplayName, clientName: InfoPlistReader.main.bundleDisplayName,
@@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.api.preferences
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<AnalyticsPreferencesState> { open class AnalyticsPreferencesStatePreviewParam : PreviewParameterProvider<AnalyticsPreferencesState> {
override val values: Sequence<AnalyticsPreferencesState> override val values: Sequence<AnalyticsPreferencesState>
get() = sequenceOf( get() = sequenceOf(
aAnalyticsPreferencesState().copy(isEnabled = true), aAnalyticsPreferencesState().copy(isEnabled = true),
@@ -66,7 +66,7 @@ fun AnalyticsPreferencesView(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) = internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStatePreviewParam::class) state: AnalyticsPreferencesState) =
ElementPreview { ElementPreview {
AnalyticsPreferencesView( AnalyticsPreferencesView(
state = state, state = state,
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_settings_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_settings_share_data">"مشاركة البيانات الإحصائية"</string>
</resources>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string> <string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string> <string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string> <string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string> <string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AnalyticsOptInStateProvider : PreviewParameterProvider<AnalyticsOptInState> { open class AnalyticsOptInStatePreviewParam : PreviewParameterProvider<AnalyticsOptInState> {
override val values: Sequence<AnalyticsOptInState> override val values: Sequence<AnalyticsOptInState>
get() = sequenceOf( get() = sequenceOf(
aAnalyticsOptInState(), aAnalyticsOptInState(),
@@ -171,7 +171,7 @@ private fun AnalyticsOptInFooter(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview { internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStatePreviewParam::class) state: AnalyticsOptInState) = ElementPreview {
AnalyticsOptInView( AnalyticsOptInView(
state = state, state = state,
onClickTerms = {}, onClickTerms = {},
@@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.Presenter
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@BindingContainer @BindingContainer
interface AnalyticsModule { interface AnalyticsBindingContainer {
@Binds @Binds
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState> fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
} }
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"لن نسجّل أو ننشئ ملفات شخصية لأي معلومات شخصية"</string>
<string name="screen_analytics_prompt_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_prompt_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_prompt_settings">"يمكنك إطفاء هذا الخيار في أي وقت"</string>
<string name="screen_analytics_prompt_third_party_sharing">"لن نشارك بياناتك مع أطراف ثالثة"</string>
<string name="screen_analytics_prompt_title">"ساعدنا على التحسن %1$s"</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string> <string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string> <string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string> <string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string> <string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string> <string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
@@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/**
* Queues the full-screen announcements shown to the user once, such as the notice about a new feature.
*
* A dismissed announcement is remembered so it is not shown again.
*/
interface AnnouncementService { interface AnnouncementService {
/**
* Queues an announcement, unless the user has already dismissed it.
*
* @param announcement the announcement to show.
*/
suspend fun showAnnouncement(announcement: Announcement) suspend fun showAnnouncement(announcement: Announcement)
/**
* Records that the user dismissed an announcement, so it is not queued again.
*
* @param announcement the announcement that was dismissed.
*/
suspend fun onAnnouncementDismissed(announcement: Announcement) suspend fun onAnnouncementDismissed(announcement: Announcement)
/** The announcements still waiting to be shown, in the order they should appear. */
fun announcementsToShowFlow(): Flow<List<Announcement>> fun announcementsToShowFlow(): Flow<List<Announcement>>
/** /**
* Use this composable to render the announcement UI in Fullscreen. * Use this composable to render the announcement UI in Fullscreen.
* Draws nothing while there is no announcement to show.
*
* @param modifier layout modifier for the container.
*/ */
@Composable @Composable
fun Render( fun Render(
@@ -11,7 +11,7 @@ package io.element.android.features.announcement.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.Announcement
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> { open class AnnouncementStatePreviewParam : PreviewParameterProvider<AnnouncementState> {
override val values: Sequence<AnnouncementState> override val values: Sequence<AnnouncementState>
get() = sequenceOf( get() = sequenceOf(
anAnnouncementState(), anAnnouncementState(),
@@ -33,7 +33,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.AnnouncementStateProvider import io.element.android.features.announcement.impl.AnnouncementStatePreviewParam
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -216,7 +216,7 @@ private fun Announcement.Fullscreen.notice(): String? = when (this) {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview { internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStatePreviewParam::class) state: AnnouncementState) = ElementPreview {
FullscreenAnnouncementView( FullscreenAnnouncementView(
state = state, state = state,
) )
@@ -8,6 +8,9 @@
package io.element.android.features.cachecleaner.api package io.element.android.features.cachecleaner.api
/**
* Removes the temporary files holding decrypted content, so plaintext media does not linger on disk.
*/
interface CacheCleaner { interface CacheCleaner {
/** /**
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages). * Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
@@ -10,6 +10,9 @@ package io.element.android.features.call.api
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
/**
* Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it.
*/
interface CurrentCallService { interface CurrentCallService {
/** /**
* The current call state flow, which will be updated when the active call changes. * The current call state flow, which will be updated when the active call changes.
@@ -11,7 +11,9 @@ package io.element.android.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController import io.element.android.features.call.impl.utils.PipController
sealed interface PictureInPictureEvent { sealed interface PictureInPictureEvent {
data object OnCallStarted : PictureInPictureEvent
data class SetPipController(val pipController: PipController) : PictureInPictureEvent data class SetPipController(val pipController: PipController) : PictureInPictureEvent
data object EnterPictureInPicture : PictureInPictureEvent data object EnterPictureInPicture : PictureInPictureEvent
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
data class SetPipOrientation(val orientation: Int?) : PictureInPictureEvent
} }
@@ -38,6 +38,9 @@ class PictureInPicturePresenter(
fun handleEvent(event: PictureInPictureEvent) { fun handleEvent(event: PictureInPictureEvent) {
when (event) { when (event) {
is PictureInPictureEvent.OnCallStarted -> {
pipController?.setupPipCallbacks()
}
is PictureInPictureEvent.SetPipController -> { is PictureInPictureEvent.SetPipController -> {
pipController = event.pipController pipController = event.pipController
} }
@@ -55,6 +58,12 @@ class PictureInPicturePresenter(
pipController?.exitPip() pipController?.exitPip()
} }
} }
is PictureInPictureEvent.SetPipOrientation -> {
Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}")
if (isPipSupported) {
pipView?.setPipOrientation(event.orientation)
}
}
} }
} }
@@ -8,8 +8,15 @@
package io.element.android.features.call.impl.pip package io.element.android.features.call.impl.pip
import android.os.Build
import androidx.annotation.RequiresApi
interface PipView { interface PipView {
@RequiresApi(Build.VERSION_CODES.O)
fun setPipOrientation(orientation: Int?)
@RequiresApi(Build.VERSION_CODES.O)
fun setPipParams() fun setPipParams()
@RequiresApi(Build.VERSION_CODES.O)
fun enterPipMode(): Boolean fun enterPipMode(): Boolean
fun hangUp() fun hangUp()
} }
@@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> { open class CallNotificationDataPreviewParam : PreviewParameterProvider<CallNotificationData> {
override val values: Sequence<CallNotificationData> override val values: Sequence<CallNotificationData>
get() = sequenceOf( get() = sequenceOf(
aCallNotificationData( aCallNotificationData(
@@ -11,7 +11,7 @@ package io.element.android.features.call.impl.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> { open class CallScreenStatePreviewParam : PreviewParameterProvider<CallScreenState> {
override val values: Sequence<CallScreenState> override val values: Sequence<CallScreenState>
get() = sequenceOf( get() = sequenceOf(
aCallScreenState(), aCallScreenState(),
@@ -118,6 +118,7 @@ internal fun CallScreenView(
if (webViewAudioManager?.isInCallMode?.get() == false) { if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode") Timber.d("URL $url is loaded, starting in-call audio mode")
webViewAudioManager?.onCallStarted() webViewAudioManager?.onCallStarted()
pipState.eventSink(PictureInPictureEvent.OnCallStarted)
} else { } else {
Timber.d("Can't start in-call audio mode since the app is already in it.") Timber.d("Can't start in-call audio mode since the app is already in it.")
} }
@@ -130,7 +131,12 @@ internal fun CallScreenView(
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it }, onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
) )
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor)) state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView) val pipController = WebViewPipController(
webView = webView,
updatePipOrientation = { orientation ->
pipState.eventSink(PictureInPictureEvent.SetPipOrientation(orientation))
}
)
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController)) pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
}, },
onDestroyWebView = { onDestroyWebView = {
@@ -260,7 +266,7 @@ private fun WebView.dispatchEscKeyEvent() {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun CallScreenViewPreview( internal fun CallScreenViewPreview(
@PreviewParameter(CallScreenStateProvider::class) state: CallScreenState, @PreviewParameter(CallScreenStatePreviewParam::class) state: CallScreenState,
) = ElementPreview { ) = ElementPreview {
CallScreenView( CallScreenView(
state = state, state = state,
@@ -11,9 +11,9 @@ package io.element.android.features.call.impl.ui
import android.Manifest import android.Manifest
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Rational
import android.view.WindowManager import android.view.WindowManager
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -49,6 +49,7 @@ import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
import io.element.android.libraries.androidutils.media.setAspectRatioFromOrientation
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocus
@@ -86,6 +87,8 @@ class ElementCallActivity :
private var eventSink: ((CallScreenEvent) -> Unit)? = null private var eventSink: ((CallScreenEvent) -> Unit)? = null
private var currentPipOrientation: Int? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -199,6 +202,9 @@ class ElementCallActivity :
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo -> val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setPipParams()
}
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode)) pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call") Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
@@ -296,11 +302,18 @@ class ElementCallActivity :
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun setPipOrientation(orientation: Int?) {
currentPipOrientation = orientation
setPictureInPictureParams(getPictureInPictureParams())
}
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun getPictureInPictureParams(): PictureInPictureParams { private fun getPictureInPictureParams(): PictureInPictureParams {
// Portrait for calls seems more appropriate as a fallback value
val orientation = currentPipOrientation ?: Configuration.ORIENTATION_PORTRAIT
return PictureInPictureParams.Builder() return PictureInPictureParams.Builder()
// Portrait for calls seems more appropriate .setAspectRatioFromOrientation(orientation)
.setAspectRatio(Rational(3, 5))
.apply { .apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true) setAutoEnterEnabled(true)
@@ -161,7 +161,7 @@ private fun ActionButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun IncomingCallScreenPreview( internal fun IncomingCallScreenPreview(
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData, @PreviewParameter(CallNotificationDataPreviewParam::class) state: CallNotificationData,
) = ElementPreview { ) = ElementPreview {
IncomingCallScreen( IncomingCallScreen(
notificationData = state, notificationData = state,
@@ -9,6 +9,7 @@
package io.element.android.features.call.impl.utils package io.element.android.features.call.impl.utils
interface PipController { interface PipController {
fun setupPipCallbacks()
suspend fun canEnterPip(): Boolean suspend fun canEnterPip(): Boolean
fun enterPip() fun enterPip()
fun exitPip() fun exitPip()
@@ -250,7 +250,7 @@ class WebViewAudioManager(
hasRegisteredCallbacks = true hasRegisteredCallbacks = true
} }
} },
) )
Timber.d("Setting androidNativeBridge javascript interface in webview") Timber.d("Setting androidNativeBridge javascript interface in webview")
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge") webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
@@ -8,7 +8,10 @@
package io.element.android.features.call.impl.utils package io.element.android.features.call.impl.utils
import android.content.res.Configuration
import android.webkit.JavascriptInterface
import android.webkit.WebView import android.webkit.WebView
import timber.log.Timber
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -18,7 +21,18 @@ import kotlin.coroutines.suspendCoroutine
*/ */
class WebViewPipController( class WebViewPipController(
private val webView: WebView, private val webView: WebView,
updatePipOrientation: (orientation: Int?) -> Unit,
) : PipController { ) : PipController {
init {
// Register the JavaScript interface ahead of time to ensure it's available when the WebView content is loaded and tries to call it
webView.addJavascriptInterface(PipBridge(updatePipOrientation), "androidPipBridge")
}
override fun setupPipCallbacks() {
Timber.d("Adding callback in controls.onPipMediaOrientationUpdate")
webView.evaluateJavascript("controls.onPipMediaOrientationUpdate = (orientation) => { androidPipBridge.setPipOrientation(orientation); };", null)
}
override suspend fun canEnterPip(): Boolean { override suspend fun canEnterPip(): Boolean {
return suspendCoroutine { continuation -> return suspendCoroutine { continuation ->
webView.evaluateJavascript("controls.canEnterPip()") { result -> webView.evaluateJavascript("controls.canEnterPip()") { result ->
@@ -36,3 +50,19 @@ class WebViewPipController(
webView.evaluateJavascript("controls.disablePip()", null) webView.evaluateJavascript("controls.disablePip()", null)
} }
} }
private class PipBridge(
private val onOrientationChange: (orientation: Int?) -> Unit,
) {
@JavascriptInterface
fun setPipOrientation(orientation: String?) {
Timber.d("Picture-in-picture orientation changed in webview, orientation: $orientation")
// This callback is used to update the picture-in-picture orientation in the WebView when it changes
val orientation = when (orientation) {
"portrait" -> Configuration.ORIENTATION_PORTRAIT
"landscape" -> Configuration.ORIENTATION_LANDSCAPE
else -> return
}
onOrientationChange(orientation)
}
}
@@ -0,0 +1,8 @@
<?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="call_invalid_audio_device_bluetooth_devices_disabled">"لا يدعم Element Call استخدام أجهزة الصوت عبر Bluetooth في إصدار Android هذا. يرجى اختيار جهاز صوت آخر."</string>
<string name="screen_incoming_call_subtitle_android">"مكالمة Element Call واردة"</string>
</resources>
@@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string> <string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string> <string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string> <string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"برنامه Element Call در این نسخه اندروید از دستگاه‌های صوتی بلوتوث پشتیبانی نمی‌کند. لطفاً دستگاه صوتی دیگری را انتخاب کنید."</string>
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string> <string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
</resources> </resources>
@@ -12,10 +12,13 @@ import io.element.android.features.call.impl.utils.PipController
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakePipController( class FakePipController(
private val setupPipCallbacksResult: () -> Unit = { lambdaError() },
private val canEnterPipResult: () -> Boolean = { lambdaError() }, private val canEnterPipResult: () -> Boolean = { lambdaError() },
private val enterPipResult: () -> Unit = { lambdaError() }, private val enterPipResult: () -> Unit = { lambdaError() },
private val exitPipResult: () -> Unit = { lambdaError() }, private val exitPipResult: () -> Unit = { lambdaError() },
) : PipController { ) : PipController {
override fun setupPipCallbacks() = setupPipCallbacksResult()
override suspend fun canEnterPip(): Boolean = canEnterPipResult() override suspend fun canEnterPip(): Boolean = canEnterPipResult()
override fun enterPip() = enterPipResult() override fun enterPip() = enterPipResult()
@@ -13,9 +13,11 @@ import io.element.android.tests.testutils.lambda.lambdaError
class FakePipView( class FakePipView(
private val setPipParamsResult: () -> Unit = { lambdaError() }, private val setPipParamsResult: () -> Unit = { lambdaError() },
private val enterPipModeResult: () -> Boolean = { lambdaError() }, private val enterPipModeResult: () -> Boolean = { lambdaError() },
private val handUpResult: () -> Unit = { lambdaError() } private val handUpResult: () -> Unit = { lambdaError() },
private val onOrientationChangeResult: (orientation: Int?) -> Unit = { lambdaError() },
) : PipView { ) : PipView {
override fun setPipParams() = setPipParamsResult() override fun setPipParams() = setPipParamsResult()
override fun enterPipMode(): Boolean = enterPipModeResult() override fun enterPipMode(): Boolean = enterPipModeResult()
override fun hangUp() = handUpResult() override fun hangUp() = handUpResult()
override fun setPipOrientation(orientation: Int?) = onOrientationChangeResult(orientation)
} }
@@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
* This will only process media when a [ContentScanner] is provided when building the client in Pro. * This will only process media when a [ContentScanner] is provided when building the client in Pro.
*/ */
fun interface ContentScannerService { fun interface ContentScannerService {
/**
* Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning.
*
* @param mediaSources the media to scan.
* @param contentValidationState where the outcome of each scan is published for the UI to observe.
*/
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
} }
@@ -21,7 +21,7 @@ import kotlinx.coroutines.CoroutineScope
@BindingContainer @BindingContainer
@ContributesTo(RoomScope::class) @ContributesTo(RoomScope::class)
object ContentScannerModule { object ContentScannerBindingContainer {
@Provides @Provides
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
fun providesContentScannerService( fun providesContentScannerService(
@@ -15,16 +15,16 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class ContentScannerModuleTest { class ContentScannerBindingContainerTest {
@Test @Test
fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest { fun `ContentScannerBindingContainer provides NoopContentScannerService when ContentScanner is null`() = runTest {
// Given // Given
val contentScanner: ContentScanner? = null val contentScanner: ContentScanner? = null
val sessionCoroutineScope = backgroundScope val sessionCoroutineScope = backgroundScope
val coroutineDispatchers = testCoroutineDispatchers() val coroutineDispatchers = testCoroutineDispatchers()
// When // When
val contentScannerService = ContentScannerModule.providesContentScannerService( val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
contentScanner = contentScanner, contentScanner = contentScanner,
coroutineScope = sessionCoroutineScope, coroutineScope = sessionCoroutineScope,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
@@ -35,14 +35,14 @@ class ContentScannerModuleTest {
} }
@Test @Test
fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest { fun `ContentScannerBindingContainer provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
// Given // Given
val contentScanner: ContentScanner? = FakeContentScanner() val contentScanner: ContentScanner? = FakeContentScanner()
val sessionCoroutineScope = backgroundScope val sessionCoroutineScope = backgroundScope
val coroutineDispatchers = testCoroutineDispatchers() val coroutineDispatchers = testCoroutineDispatchers()
// When // When
val contentScannerService = ContentScannerModule.providesContentScannerService( val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
contentScanner = contentScanner, contentScanner = contentScanner,
coroutineScope = sessionCoroutineScope, coroutineScope = sessionCoroutineScope,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
@@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint { interface CreateRoomEntryPoint : FeatureEntryPoint {
/**
* Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens.
*/
interface Builder { interface Builder {
/**
* @param isSpace true to create a space rather than a room.
*/
fun setIsSpace(isSpace: Boolean): Builder fun setIsSpace(isSpace: Boolean): Builder
/**
* @param parentSpaceId the space the new room should be added to once created.
*/
fun setParentSpace(parentSpaceId: RoomId): Builder fun setParentSpace(parentSpaceId: RoomId): Builder
/** Builds the node with the options configured so far. */
fun build(): Node fun build(): Node
} }
@@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
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
@@ -86,7 +86,7 @@ private fun AddPeopleTopBar(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview { internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStatePreviewParam::class) state: InvitePeopleState) = ElementPreview {
AddPeopleView( AddPeopleView(
state = state, state = state,
invitePeopleView = {}, invitePeopleView = {},
@@ -21,7 +21,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState
import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> { open class ConfigureRoomStatePreviewParam : PreviewParameterProvider<ConfigureRoomState> {
override val values: Sequence<ConfigureRoomState> override val values: Sequence<ConfigureRoomState>
get() = sequenceOf( get() = sequenceOf(
aConfigureRoomState(), aConfigureRoomState(),
@@ -349,12 +349,12 @@ private fun RoomJoinRuleOptions(
@PreviewWithLargeHeight @PreviewWithLargeHeight
@Composable @Composable
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
ElementPreviewLight { ContentToPreview(state) } ElementPreviewLight { ContentToPreview(state) }
@PreviewWithLargeHeight @PreviewWithLargeHeight
@Composable @Composable
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
ElementPreviewDark { ContentToPreview(state) } ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage @ExcludeFromCoverage
@@ -0,0 +1,36 @@
<?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_error_creating_space">"تعذّر إنشاء المساحة بسبب خطأ غير معروف. حاول مرة أخرى لاحقًا."</string>
<string name="screen_create_room_name_placeholder">"إضافة اسم…"</string>
<string name="screen_create_room_new_room_title">"غرفة جديدة"</string>
<string name="screen_create_room_new_space_title">"مساحة جديدة"</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_short_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"يمكن لأي شخص طلب الانضمام، لكن يجب أن يقبل الطلب مسؤول أو مشرف."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"السماح بطلبات الانضمام"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام، لكن يجب على الآخرين طلب الوصول."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"طلب الانضمام"</string>
<string name="screen_create_room_room_access_section_private_option_description">"يمكن للمدعوين فقط الانضمام."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خاصة"</string>
<string name="screen_create_room_room_access_section_public_option_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"قياسي"</string>
<string name="screen_create_room_room_access_section_title">"من يمكنه الوصول"</string>
<string name="screen_create_room_room_address_section_footer">"تحتاج إلى عنوان حتى تظهر الغرفة في الدليل العام."</string>
<string name="screen_create_room_room_address_section_title">"العنوان"</string>
<string name="screen_create_room_room_visibility_section_title">"ظهور الغرفة"</string>
<string name="screen_create_room_space_selection_no_space_description">"(لا توجد مساحة)"</string>
<string name="screen_create_room_space_selection_no_space_option">"عدم الإضافة إلى مساحة"</string>
<string name="screen_create_room_space_selection_no_space_title">"لم يتم اختيار أي مساحة"</string>
<string name="screen_create_room_space_selection_sheet_title">"إضافة إلى مساحة"</string>
<string name="screen_create_room_topic_label">"الموضوع (اختياري)"</string>
<string name="screen_create_room_topic_placeholder">"إضافة وصف…"</string>
</resources>
@@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Nueva sala"</string> <string name="screen_create_room_action_create_room">"Nueva sala"</string>
<string name="screen_create_room_add_people_title">"Invitar personas"</string> <string name="screen_create_room_add_people_title">"Invitar personas"</string>
<string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string> <string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string>
<string name="screen_create_room_error_creating_space">"No se pudo crear el espacio debido a un error desconocido. Inténtalo de nuevo más tarde."</string>
<string name="screen_create_room_name_placeholder">"Añadir nombre…"</string>
<string name="screen_create_room_new_room_title">"Nueva sala"</string>
<string name="screen_create_room_new_space_title">"Nuevo espacio"</string>
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string> <string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string>
<string name="screen_create_room_private_option_title">"Privado"</string>
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala. <string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string> Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_public_option_short_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Cualquiera en %1$s puede unirse, pero el resto debe solicitar acceso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Solicitar unirse"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Solo las personas invitadas pueden unirse."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privado"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string> <string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Cualquiera de %1$s puede unirse."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Estándar"</string>
<string name="screen_create_room_room_access_section_title">"Quién tiene acceso"</string>
<string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string> <string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string>
<string name="screen_create_room_room_address_section_title">"Dirección"</string> <string name="screen_create_room_room_address_section_title">"Dirección"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string> <string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_space_selection_no_space_description">"(ningún espacio)"</string>
<string name="screen_create_room_space_selection_no_space_option">"No añadir a un espacio"</string>
<string name="screen_create_room_space_selection_no_space_title">"No se ha seleccionado ningún espacio"</string>
<string name="screen_create_room_space_selection_sheet_title">"Añadir a un espacio"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string> <string name="screen_create_room_topic_label">"Tema (opcional)"</string>
<string name="screen_create_room_topic_placeholder">"Añadir descripción…"</string>
</resources> </resources>
@@ -3,12 +3,34 @@
<string name="screen_create_room_action_create_room">"اتاق جدید"</string> <string name="screen_create_room_action_create_room">"اتاق جدید"</string>
<string name="screen_create_room_add_people_title">"دعوت افراد"</string> <string name="screen_create_room_add_people_title">"دعوت افراد"</string>
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string> <string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
<string name="screen_create_room_error_creating_space">"به دلیل خطای ناشناخته‌ای، فضا ایجاد نشد. بعداً دوباره امتحان کنید."</string>
<string name="screen_create_room_name_placeholder">"افزودن نام…"</string>
<string name="screen_create_room_new_room_title">"اتاق جدید"</string>
<string name="screen_create_room_new_space_title">"فضای جدید"</string>
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</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 name="screen_create_room_public_option_description">"هرکسی می‌تواند اتاق را بیابد.
می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string> می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string> <string name="screen_create_room_public_option_short_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_public_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string> <string name="screen_create_room_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"هر کسی می‌تواند درخواست عضویت بدهد، اما یک مدیر یا ناظر باید درخواست را بپذیرد."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"اجازه درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"هر کسی در %1$s می‌تواند عضو شود، اما بقیه باید درخواست دسترسی بدهند."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خصوصی"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"هر کسی که در %1$s باشد می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"استاندارد"</string>
<string name="screen_create_room_room_access_section_title">"چه کسی دسترسی دارد"</string>
<string name="screen_create_room_room_address_section_footer">"برای اینکه بتوانید آن را در فهرست عمومی مشاهده کنید، به یک آدرس نیاز دارید."</string>
<string name="screen_create_room_room_address_section_title">"آدرس"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string> <string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_space_selection_no_space_description">"(بدون فضا)"</string>
<string name="screen_create_room_space_selection_no_space_option">"به یک فضا اضافه نکنید"</string>
<string name="screen_create_room_space_selection_no_space_title">"فضایی انتخاب نشده است"</string>
<string name="screen_create_room_space_selection_sheet_title">"به فضا اضافه کنید"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string> <string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_create_room_topic_placeholder">"اضافه کردن توضیحات…"</string>
</resources> </resources>
@@ -11,7 +11,7 @@ package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDeactivationState> { open class AccountDeactivationStatePreviewParam : PreviewParameterProvider<AccountDeactivationState> {
private val filledForm = aDeactivateFormState(eraseData = true, password = "password") private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
override val values: Sequence<AccountDeactivationState> override val values: Sequence<AccountDeactivationState>
get() = sequenceOf( get() = sequenceOf(
@@ -299,16 +299,16 @@ private fun Content(
} }
/** /**
* Ensure that the string does not contain any new line characters, which can happen when pasting values. * Ensure that the string does not contain any line separator, which can happen when pasting values.
*/ */
private fun String.sanitize(): String { private fun String.sanitize(): String {
return replace("\n", "") return filterNot { it == '\n' || it == '\r' }
} }
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun AccountDeactivationViewPreview( internal fun AccountDeactivationViewPreview(
@PreviewParameter(AccountDeactivationStateProvider::class) state: AccountDeactivationState, @PreviewParameter(AccountDeactivationStatePreviewParam::class) state: AccountDeactivationState,
) = ElementPreview { ) = ElementPreview {
AccountDeactivationView( AccountDeactivationView(
state, state,
@@ -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">"يرجى تأكيد رغبتك في حذف حسابك. لا يمكن التراجع عن هذا الإجراء."</string>
<string name="screen_deactivate_account_delete_all_messages">"حذف جميع رسائلي"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"تحذير: قد يرى المستخدمون في المستقبل محادثات غير مكتملة."</string>
<string name="screen_deactivate_account_description">"حذف حسابك %1$s، وسيؤدي إلى:"</string>
<string name="screen_deactivate_account_description_bold_part">"إجراء لا رجعة فيه"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s (لن تتمكن من تسجيل الدخول مرة أخرى، ولا يمكن إعادة استخدام معرّفك)."</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_list_item_4">"ستظل رسائلك مرئية للمستخدمين المسجلين، لكنها لن تكون متاحة للمستخدمين الجدد أو غير المسجلين إذا اخترت حذفها."</string>
<string name="screen_deactivate_account_title">"حذف الحساب"</string>
</resources>

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