Merge branch 'release/26.03.3'

This commit is contained in:
Benoit Marty 2026-03-10 15:36:23 +01:00
commit 75841761cc
241 changed files with 2794 additions and 1612 deletions

View file

@ -13,6 +13,8 @@ updates:
open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
cooldown:
default-days: 7
# Updates for Gradle dependencies used in the app
- package-ecosystem: "gradle"
directory: "/"
@ -21,3 +23,5 @@ updates:
open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
cooldown:
default-days: 7

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -16,6 +18,9 @@ jobs:
build:
name: Build APKs
runs-on: ubuntu-latest
permissions:
# For NejcZdovc/comment-pr
pull-requests: write
strategy:
matrix:
variant: [debug, release, nightly]
@ -39,18 +44,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs
@ -68,7 +74,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-debug
path: |

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -41,11 +43,12 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
@ -53,12 +56,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK
@ -76,7 +79,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-enterprise-debug
path: |

View file

@ -2,6 +2,8 @@ name: Danger CI
on: [pull_request, merge_group]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
@ -9,7 +11,9 @@ jobs:
# Skip in forks, it doesn't work even with the fallback token
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:

View file

@ -2,11 +2,13 @@ name: Community PR notice
on:
workflow_dispatch:
pull_request_target:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
- reopened
permissions: {}
jobs:
welcome:
runs-on: ubuntu-latest
@ -15,7 +17,7 @@ jobs:
if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name
steps:
- name: Add auto-generated commit warning
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.createComment({

View file

@ -5,6 +5,8 @@ on:
# At 00:00 on every Tuesday UTC
- cron: '0 0 * * 2'
permissions: {}
jobs:
generate-github-pages:
runs-on: ubuntu-latest
@ -14,16 +16,16 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Run World screenshots generation script

View file

@ -5,14 +5,18 @@ on:
schedule:
- cron: "0 0 * * *"
permissions: {}
jobs:
update-gradle-wrapper:
runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:

View file

@ -5,6 +5,8 @@ on:
workflow_dispatch:
pull_request:
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -36,18 +38,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
- uses: actions/setup-java@v5
persist-credentials: false
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
@ -57,7 +60,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-apk-maestro
path: |
@ -75,14 +78,15 @@ jobs:
concurrency:
group: maestro-test
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
persist-credentials: false
- name: Download APK artifact from previous job
uses: actions/download-artifact@v8
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: elementx-apk-maestro
- name: Enable KVM group perms
@ -94,7 +98,7 @@ jobs:
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: Run Maestro tests in emulator
id: maestro_test
uses: reactivecircus/android-emulator-runner@v2
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
continue-on-error: true
env:
MAESTRO_USERNAME: maestroelement
@ -115,7 +119,7 @@ jobs:
script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
- name: Upload test results
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results
path: |

View file

@ -6,6 +6,8 @@ on:
# Every nights at 4
- cron: "0 4 * * *"
permissions: {}
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
@ -30,9 +32,11 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'

View file

@ -6,6 +6,8 @@ on:
# Every nights at 5
- cron: "0 5 * * *"
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -35,13 +37,13 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: false
@ -56,7 +58,7 @@ jobs:
- name: ✅ Upload kover report
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: kover-results
path: |
@ -74,21 +76,23 @@ jobs:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: dependency-analysis
path: build/reports/dependency-check-report.html

View file

@ -5,6 +5,8 @@ on:
tags:
- 'v*'
permissions: {}
jobs:
post-release:
runs-on: ubuntu-latest
@ -13,7 +15,7 @@ jobs:
steps:
- name: Trigger pipeline
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }}
script: |

View file

@ -2,11 +2,13 @@ name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
workflow_call:
workflow_call: # zizmor: ignore[dangerous-triggers]
secrets:
ELEMENT_BOT_TOKEN:
required: true
permissions: {}
jobs:
prevent-blocked:
name: Prevent blocked
@ -15,7 +17,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@ -39,7 +41,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.addLabels({
@ -58,7 +60,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.createComment({

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ main, develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -31,7 +33,9 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -47,9 +51,11 @@ jobs:
name: Search for invalid screenshot files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid screenshot files
@ -59,18 +65,20 @@ jobs:
name: Search for invalid dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid dependencies
@ -85,11 +93,12 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -99,19 +108,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: konsist-report
path: |
@ -125,11 +134,12 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -139,12 +149,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
@ -158,11 +168,12 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -172,12 +183,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@ -188,7 +199,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: linting-report
path: |
@ -202,11 +213,12 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -216,19 +228,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: detekt-report
path: |
@ -242,11 +254,12 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -256,37 +269,38 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ktlint-report
path: |
**/build/reports/**/*.*
knit:
name: Knit checks
docs:
name: Doc checks
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -295,17 +309,9 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
- name: Run docs check
# This is equivalent to `./gradlew checkDocs`, but we avoid having to install java and gradle
run: python3 ./tools/docs/generate_toc.py --verify ./*.md docs/**/*.md
# Note: to auto fix issues you can use the following command:
# shellcheck -f diff <files> | git apply
@ -313,25 +319,39 @@ jobs:
name: Check shell scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run shellcheck
uses: ludeeus/action-shellcheck@2.0.0
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0
with:
severity: warning
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
upload_reports:
name: Project Check Suite
runs-on: ubuntu-latest
needs: [konsist, lint, ktlint, detekt]
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Download reports from previous jobs
uses: actions/download-artifact@v8
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
- name: Prepare Danger
if: always()
run: |

View file

@ -5,6 +5,8 @@ on:
pull_request:
types: [ labeled ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
@ -48,13 +50,13 @@ jobs:
with:
persist-credentials: false
- name: ☕️ Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots

View file

@ -5,6 +5,8 @@ on:
push:
branches: [ main ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -32,14 +34,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -53,7 +57,7 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-app-gplay-bundle-unsigned
path: |
@ -67,7 +71,9 @@ jobs:
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
@ -76,12 +82,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -89,7 +95,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-enterprise-app-gplay-bundle-unsigned
path: |
@ -116,14 +122,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -131,7 +139,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: elementx-app-fdroid-apks-unsigned
path: |

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ main, develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
@ -36,18 +38,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures

View file

@ -4,13 +4,15 @@ on:
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
only-labels: "X-Needs-Info"
days-before-issue-stale: 30

View file

@ -5,24 +5,28 @@ on:
# At 00:00 on every Monday UTC
- cron: '0 0 * * 1'
permissions: {}
jobs:
sync-localazy:
runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Setup Localazy

View file

@ -5,6 +5,8 @@ on:
# At 00:00 on every Monday UTC
- cron: '0 0 * * 1'
permissions: {}
jobs:
sync-sas-strings:
runs-on: ubuntu-latest
@ -12,9 +14,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
# No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Install Prerequisite dependencies

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ main, develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
@ -61,12 +63,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
@ -75,7 +77,7 @@ jobs:
- name: 🚫 Upload kover failed coverage reports
if: failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: kover-error-report
path: |
@ -87,7 +89,7 @@ jobs:
- name: 🚫 Upload test results on error
if: failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: tests-and-screenshot-tests-results
path: |

View file

@ -4,11 +4,13 @@ on:
issues:
types: [ opened ]
permissions: {}
jobs:
triage-new-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/91
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -4,6 +4,8 @@ on:
issues:
types: [labeled]
permissions: {}
jobs:
move_element_x_issues:
name: ElementX issues to ElementX project board
@ -12,7 +14,7 @@ jobs:
if: >
github.repository == 'element-hq/element-x-android'
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/43
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -21,14 +23,16 @@ jobs:
name: Move triaged needs info issues on board
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
id: addItem
with:
project-url: https://github.com/orgs/element-hq/projects/91
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
labeled: X-Needs-Info
- name: Print itemId
run: echo ${{ steps.addItem.outputs.itemId }}
run: echo ${STEPS_ADDITEM_OUTPUTS_ITEMID}
env:
STEPS_ADDITEM_OUTPUTS_ITEMID: ${{ steps.addItem.outputs.itemId }}
- uses: kalgurn/update-project-item-status@31e54df46a2cdaef4f85c31ac839fbcd2fd7c3a2 # 0.0.3
if: ${{ steps.addItem.outputs.itemId }}
with:
@ -43,7 +47,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/73
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -54,7 +58,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/57
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -66,7 +70,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'Team: QA') ||
contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/69
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -77,7 +81,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/89
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -2,6 +2,8 @@ name: Validate Git LFS
on: [pull_request, merge_group]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest

View file

@ -1,3 +1,56 @@
Changes in Element X v26.03.2
=============================
## Hotfix release
This release is out of our normal release cycle because we detected an important issue that could happen when instantiating the cryptographic DB and would result in the room sync not working.
## What's Changed
### 🙌 Improvements
* Floating toolbar by @bmarty in https://github.com/element-hq/element-x-android/pull/6147
### 🐛 Bugfixes
* Ensure that redacted event from encrypted room does not trigger a fallback notification by @bmarty in https://github.com/element-hq/element-x-android/pull/6241
* Add `MediaSource.safeUrl` for removing invalid fragment part from URLs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6035
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6269
### 🧱 Build
* Fix nightly CI issues by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6263
* CI: Add failed tests to summary by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6271
* Make 'room list catch-up' analytics transaction network aware by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6233
* Adjust the build-rust-sdk script to allow non-interactive use by @andybalaam in https://github.com/element-hq/element-x-android/pull/6281
### Dependency upgrades
* Update metro to v0.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6245
* Update dependency com.posthog:posthog-android to v3.34.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6251
* Update metro to v0.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6255
* Update coil to v3.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6243
* Update dependency io.element.android:element-call-embedded to v0.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6244
* Update dependency com.posthog:posthog-android to v3.34.2 - autoclosed by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6254
* Update dependencyAnalysis to v3.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6256
* Update GitHub Artifact Actions (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6260
* Update dependency com.google.firebase:firebase-bom to v34.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6262
* Update dependency androidx.compose:compose-bom to v2026.02.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6267
* Update dependency com.posthog:posthog-android to v3.34.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6272
* Update metro to v0.11.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6270
* Update dependencyAnalysis to v3.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6259
* Sync compound tokens https://github.com/element-hq/compound-design-tokens/releases/tag/v6.10.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/6273
* Update dependency io.sentry:sentry-android to v8.34.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6280
* Update dependency org.matrix.rustcomponents:sdk-android to v26.03.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6282
* Update dependency org.matrix.rustcomponents:sdk-android to v26.03.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6287
* Update plugin ktlint to v14.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6288
* Update dependency org.unifiedpush.android:connector to v3.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6285
### Others
* Add some DB optimizations by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6249
* Check if network access if blocked when fetching notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6247
* Bottom bar iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6264
* Use `ShareIntentHandler` early to avoid distributing the whole intent by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6274
* Simplify push notification flow by using locally stored values for pending pushes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6258
* Fix typed text becoming invisible when composing long messages by @timurgilfanov in https://github.com/element-hq/element-x-android/pull/6284
## New Contributors
* @timurgilfanov made their first contribution in https://github.com/element-hq/element-x-android/pull/6284
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.0...v26.03.2
Changes in Element X v26.03.0
=============================

View file

@ -16,7 +16,7 @@
* [Code quality](#code-quality)
* [detekt](#detekt)
* [ktlint](#ktlint)
* [knit](#knit)
* [checkDocs](#checkdocs)
* [lint](#lint)
* [Unit tests](#unit-tests)
* [konsist](#konsist)
@ -123,13 +123,13 @@ Note that you can run
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
#### knit
#### checkDocs
[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files.
`checkDocs` is a Gradle task which checks markdown files on the project to ensure their table of contents is up to date. It uses `tools/docs/generate_toc.py --verify` under the hood, and has a counterpart `generateDocsToc` task which runs `tools/docs/generate_toc.py` to update the table of contents of markdown files.
So everytime the toc should be updated, just run
<pre>
./gradlew knit
./gradlew generateDocsToc
</pre>
and commit the changes.
@ -137,7 +137,7 @@ and commit the changes.
The CI will check that markdown files are up to date by running
<pre>
./gradlew knitCheck
./gradlew checkDocs
</pre>
#### lint

View file

@ -33,7 +33,6 @@ plugins {
alias(libs.plugins.kotlin.android)
// When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit)
id("kotlin-parcelize")
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
@ -250,26 +249,6 @@ androidComponents {
configureLicensesTasks(reportingExtension)
}
// Knit
apply {
plugin("kotlinx-knit")
}
knit {
files = fileTree(project.rootDir) {
include(
"**/*.md",
"**/*.kt",
"*/*.kts",
)
exclude(
"**/build/**",
"*/.gradle/**",
"**/CHANGES.md",
)
}
}
setupDependencyInjection()
dependencies {

View file

@ -26,7 +26,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.compound.colors.SemanticColorsLightDark
@ -35,6 +34,7 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.libraries.architecture.appyx.DebugNavStateNodeHost
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp
@ -100,7 +100,9 @@ class MainActivity : NodeActivity() {
@Composable
private fun MainNodeHost() {
NodeHost(integrationPoint = appyxV1IntegrationPoint) {
// TODO this is a temporary helper to capture the nav state in a more readable format for crash reports
// Revert to `NodeHost` once this is fixed
DebugNavStateNodeHost(integrationPoint = appyxV1IntegrationPoint) {
MainNode(
it,
plugins = listOf(
@ -110,7 +112,7 @@ class MainActivity : NodeActivity() {
mainNode = node
mainNode.handleIntent(intent)
}
}
},
),
context = applicationContext
)

View file

@ -175,12 +175,23 @@ tasks.register("runQualityChecks") {
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
}
dependsOn(":app:knitCheck")
dependsOn("checkDocs")
// Make sure all checks run even if some fail
gradle.startParameter.isContinueOnFailure = true
}
// Register Markdown documentation check task.
tasks.register("checkDocs", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", "--verify", *inputs.files.map { it.path }.toTypedArray())
}
// Register Markdown documentation TOC generation task.
tasks.register("generateDocsToc", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", *inputs.files.map { it.path }.toTypedArray())
}
// Make sure to delete old screenshots before recording new ones
subprojects {
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")

View file

@ -10,8 +10,8 @@ This document explains how to install Element X Android from a Github Release.
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
* [Installing from the App Bundle](#installing-from-the-app-bundle)
* [Requirements](#requirements)
* [Steps](#steps)
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
* [Steps](#steps-1)
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone-1)
<!--- END -->

View file

@ -2,11 +2,11 @@
<!--- TOC -->
* [Installing from GitHub](#installing-from-github)
* [Create a GitHub token](#create-a-github-token)
* [Provide artifact URL](#provide-artifact-url)
* [Next steps](#next-steps)
* [Future improvement](#future-improvement)
* [Installing from GitHub](#installing-from-github)
* [Create a GitHub token](#create-a-github-token)
* [Provide artifact URL](#provide-artifact-url)
* [Next steps](#next-steps)
* [Future improvement](#future-improvement)
<!--- END -->

View file

@ -8,7 +8,7 @@
* [Stop Synapse](#stop-synapse)
* [Troubleshoot](#troubleshoot)
* [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver)
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost8080")
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-unable-to-contact-localhost8080)
* [virtualenv command fails](#virtualenv-command-fails)
<!--- END -->

View file

@ -5,11 +5,11 @@ This document aims to describe how Element android displays notifications to the
<!--- TOC -->
* [Prerequisites Knowledge](#prerequisites-knowledge)
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?)
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver)
* [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification)
* [Push VS Notification](#push-vs-notification)
* [Push in the matrix federated world](#push-in-the-matrix-federated-world)
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?)
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client)
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
* [Background processing limitations](#background-processing-limitations)
* [Element Notification implementations](#element-notification-implementations)

View file

@ -3,23 +3,23 @@
<!--- TOC -->
* [Introduction](#introduction)
* [Who should read this document?](#who-should-read-this-document?)
* [Who should read this document?](#who-should-read-this-document)
* [Submitting PR](#submitting-pr)
* [Who can submit pull requests?](#who-can-submit-pull-requests?)
* [Who can submit pull requests?](#who-can-submit-pull-requests)
* [Humans](#humans)
* [Draft PR?](#draft-pr?)
* [Draft PR?](#draft-pr)
* [Base branch](#base-branch)
* [PR Review Assignment](#pr-review-assignment)
* [PR review time](#pr-review-time)
* [Re-request PR review](#re-request-pr-review)
* [When create split PR?](#when-create-split-pr?)
* [When create split PR?](#when-create-split-pr)
* [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr)
* [Bots](#bots)
* [Renovate](#renovate)
* [Gradle wrapper](#gradle-wrapper)
* [Sync analytics plan](#sync-analytics-plan)
* [Reviewing PR](#reviewing-pr)
* [Who can review pull requests?](#who-can-review-pull-requests?)
* [Who can review pull requests?](#who-can-review-pull-requests)
* [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr)
* [Rules](#rules)
* [Check the form](#check-the-form)
@ -29,7 +29,7 @@
* [Check the commit](#check-the-commit)
* [Check the substance](#check-the-substance)
* [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr)
* [What happen to the issue(s)?](#what-happen-to-the-issues?)
* [What happen to the issue(s)?](#what-happen-to-the-issues)
* [Merge conflict](#merge-conflict)
* [When and who can merge PR](#when-and-who-can-merge-pr)
* [Merge type](#merge-type)

@ -1 +1 @@
Subproject commit 1fd0d297d944186e3af2773e1c5db2938d60f74b
Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463

View file

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

View file

@ -26,9 +26,10 @@ sealed interface CallType : NodeInputs, Parcelable {
data class RoomCall(
val sessionId: SessionId,
val roomId: RoomId,
val isAudioCall: Boolean
) : CallType {
override fun toString(): String {
return "RoomCall(sessionId=$sessionId, roomId=$roomId)"
return "RoomCall(sessionId=$sessionId, roomId=$roomId, isAudioCall=$isAudioCall)"
}
}
}

View file

@ -58,6 +58,7 @@ class DefaultElementCallEntryPoint(
expirationTimestamp = expirationTimestamp,
notificationChannelId = notificationChannelId,
textContent = textContent,
audioOnly = callType.isAudioCall
)
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
}

View file

@ -29,4 +29,5 @@ data class CallNotificationData(
val textContent: String?,
// Expiration timestamp in millis since epoch
val expirationTimestamp: Long,
val audioOnly: Boolean,
) : Parcelable

View file

@ -69,6 +69,7 @@ class RingingCallNotificationCreator(
timestamp: Long,
expirationTimestamp: Long,
textContent: String?,
audioOnly: Boolean,
): Notification? {
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val imageLoader = imageLoaderHolder.get(matrixClient)
@ -88,7 +89,7 @@ class RingingCallNotificationCreator(
.setImportant(true)
.build()
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId))
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId, isAudioCall = audioOnly))
val notificationData = CallNotificationData(
sessionId = sessionId,
roomId = roomId,
@ -101,6 +102,7 @@ class RingingCallNotificationCreator(
timestamp = timestamp,
textContent = textContent,
expirationTimestamp = expirationTimestamp,
audioOnly = audioOnly,
)
val declineIntent = PendingIntentCompat.getBroadcast(
@ -127,7 +129,11 @@ class RingingCallNotificationCreator(
.setSmallIcon(CommonDrawables.ic_notification)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true))
.setStyle(
NotificationCompat.CallStyle
.forIncomingCall(caller, declineIntent, answerIntent)
.setIsVideo(!audioOnly)
)
.addPerson(caller)
.setAutoCancel(true)
.setWhen(timestamp)

View file

@ -45,8 +45,8 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
callType = CallType.RoomCall(
sessionId = notificationData.sessionId,
roomId = notificationData.roomId,
),
notificationData = notificationData,
isAudioCall = notificationData.audioOnly
)
)
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.features.call.impl.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> {
override val values: Sequence<CallNotificationData>
get() = sequenceOf(
aCallNotificationData(
audioOnly = false
),
aCallNotificationData(
audioOnly = true
),
)
}
internal fun aCallNotificationData(
audioOnly: Boolean
): CallNotificationData {
return CallNotificationData(
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
audioOnly = audioOnly
)
}

View file

@ -226,6 +226,7 @@ class CallScreenPresenter(
sessionId = inputs.sessionId,
roomId = inputs.roomId,
clientId = UUID.randomUUID().toString(),
isAudioCall = inputs.isAudioCall,
languageTag = languageTag,
theme = theme,
).getOrThrow()

View file

@ -112,7 +112,13 @@ class IncomingCallActivity : AppCompatActivity() {
}
private fun onAnswer(notificationData: CallNotificationData) {
elementCallEntryPoint.startCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
elementCallEntryPoint.startCall(
CallType.RoomCall(
notificationData.sessionId,
notificationData.roomId,
isAudioCall = notificationData.audioOnly
)
)
}
private fun onCancel() {

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@ -45,10 +46,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings
/**
@ -103,7 +100,7 @@ internal fun IncomingCallScreen(
ActionButton(
size = 64.dp,
onClick = { onAnswer(notificationData) },
icon = CompoundIcons.VoiceCallSolid(),
icon = if (notificationData.audioOnly) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
title = stringResource(CommonStrings.action_accept),
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle
@ -163,21 +160,11 @@ private fun ActionButton(
@PreviewsDayNight
@Composable
internal fun IncomingCallScreenPreview() = ElementPreview {
internal fun IncomingCallScreenPreview(
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
) = ElementPreview {
IncomingCallScreen(
notificationData = CallNotificationData(
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
),
notificationData = state,
onAnswer = {},
onCancel = {},
)

View file

@ -146,6 +146,7 @@ class DefaultActiveCallManager(
callType = CallType.RoomCall(
sessionId = notificationData.sessionId,
roomId = notificationData.roomId,
isAudioCall = notificationData.audioOnly,
),
callState = CallState.Ringing(notificationData),
)
@ -273,6 +274,7 @@ class DefaultActiveCallManager(
timestamp = notificationData.timestamp,
textContent = notificationData.textContent,
expirationTimestamp = notificationData.expirationTimestamp,
audioOnly = notificationData.audioOnly,
) ?: return
runCatchingExceptions {
notificationManagerCompat.notify(

View file

@ -16,6 +16,7 @@ interface CallWidgetProvider {
suspend fun getWidget(
sessionId: SessionId,
roomId: RoomId,
isAudioCall: Boolean,
clientId: String,
languageTag: String?,
theme: String?,

View file

@ -32,6 +32,7 @@ class DefaultCallWidgetProvider(
override suspend fun getWidget(
sessionId: SessionId,
roomId: RoomId,
isAudioCall: Boolean,
clientId: String,
languageTag: String?,
theme: String?,
@ -50,6 +51,7 @@ class DefaultCallWidgetProvider(
baseUrl = baseUrl,
encrypted = isEncrypted,
direct = room.isDm(),
isAudioCall = isAudioCall,
hasActiveCall = roomInfo.hasRoomCall,
)
val callUrl = room.generateWidgetWebViewUrl(

View file

@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest {
@Test
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
val entryPoint = createEntryPoint()
entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false))
val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java)
val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
@ -53,7 +53,7 @@ class DefaultElementCallEntryPointTest {
val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
entryPoint.handleIncomingCall(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false),
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = "roomName",

View file

@ -65,7 +65,33 @@ class RingingCallNotificationCreatorTest {
getUserIconLambda.assertions().isCalledOnce()
}
private suspend fun RingingCallNotificationCreator.createTestNotification() = createNotification(
@Test
fun `createNotification - use the correct style for video call`() = runTest {
val notificationCreator = createRingingCallNotificationCreator(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
)
val notification = notificationCreator.createTestNotification()
assertThat(notification?.category).isEqualTo("call")
val acceptAction = notification?.actions?.get(1)
assertThat(acceptAction?.title?.toString()).isEqualTo("Video")
}
@Test
fun `createNotification - use the correct style for audio call`() = runTest {
val notificationCreator = createRingingCallNotificationCreator(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
)
val notification = notificationCreator.createTestNotification(audioOnly = true)
assertThat(notification?.category).isEqualTo("call")
val acceptAction = notification?.actions?.get(1)
assertThat(acceptAction?.title?.toString()).isEqualTo("Answer")
}
private suspend fun RingingCallNotificationCreator.createTestNotification(audioOnly: Boolean = false) = createNotification(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
@ -77,6 +103,7 @@ class RingingCallNotificationCreatorTest {
timestamp = 0L,
expirationTimestamp = 20L,
textContent = "textContent",
audioOnly = audioOnly
)
private fun createRingingCallNotificationCreator(

View file

@ -90,7 +90,7 @@ class CallScreenPresenterTest {
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
widgetProvider = widgetProvider,
screenTracker = FakeScreenTracker(analyticsLambda),
@ -123,7 +123,7 @@ class CallScreenPresenterTest {
fun `present - set message interceptor, send and receive messages`() = runTest {
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
screenTracker = FakeScreenTracker {},
)
@ -154,7 +154,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -188,7 +188,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -223,7 +223,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -260,7 +260,7 @@ class CallScreenPresenterTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
@ -300,7 +300,7 @@ class CallScreenPresenterTest {
val matrixClient = FakeMatrixClient(syncService = syncService)
val appForegroundStateService = FakeAppForegroundStateService()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),

View file

@ -27,6 +27,7 @@ class CallTypeTest {
CallType.RoomCall(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
isAudioCall = false,
).getSessionId()
).isEqualTo(A_SESSION_ID)
}
@ -38,7 +39,7 @@ class CallTypeTest {
@Test
fun `RoomCall stringification does not contain the URL`() {
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString())
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)")
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false).toString())
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)")
}
}

View file

@ -80,6 +80,7 @@ class DefaultActiveCallManagerTest {
callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId,
isAudioCall = false,
),
callState = CallState.Ringing(callNotificationData)
)
@ -91,6 +92,28 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.notify(notificationId, any()) }
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `registerIncomingCall - sets the incoming audio call as active`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
val callNotificationData = aCallNotificationData(audioOnly = true)
manager.registerIncomingCall(callNotificationData)
assertThat(manager.activeCall.value).isEqualTo(
ActiveCall(
callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId,
isAudioCall = true,
),
callState = CallState.Ringing(callNotificationData)
)
)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
@ -165,7 +188,7 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false))
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse()
@ -192,7 +215,7 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData)
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false))
coVerify {
room.declineCall(notificationEventId = notificationData.eventId)
@ -219,7 +242,7 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
// Do not register the incoming call, so the manager doesn't know about it
manager.hangUpCall(
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId),
callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false),
notificationData = notificationData,
)
coVerify {
@ -321,12 +344,13 @@ class DefaultActiveCallManagerTest {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
assertThat(manager.activeCall.value).isNull()
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, true))
assertThat(manager.activeCall.value).isEqualTo(
ActiveCall(
callType = CallType.RoomCall(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
isAudioCall = true,
),
callState = CallState.InCall,
)
@ -429,6 +453,7 @@ class DefaultActiveCallManagerTest {
callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId,
isAudioCall = false,
),
callState = CallState.Ringing(callNotificationData)
)

View file

@ -31,7 +31,7 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if the session does not exist`() = runTest {
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
}
@Test
@ -40,7 +40,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, null)
}
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, true, "clientId", "languageTag", "theme").isFailure).isTrue()
}
@Test
@ -52,7 +52,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room)
}
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
}
@Test
@ -65,7 +65,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room)
}
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isFailure).isTrue()
}
@Test
@ -78,7 +78,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room)
}
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) })
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
}
@Test
@ -101,7 +101,7 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
activeRoomsHolder = activeRoomsHolder
)
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue()
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme").isSuccess).isTrue()
}
@Test
@ -122,7 +122,7 @@ class DefaultCallWidgetProviderTest {
callWidgetSettingsProvider = settingsProvider,
appPreferencesStore = preferencesStore,
)
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
provider.getWidget(A_SESSION_ID, A_ROOM_ID, false, "clientId", "languageTag", "theme")
assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io")
}

View file

@ -23,6 +23,7 @@ class FakeCallWidgetProvider(
override suspend fun getWidget(
sessionId: SessionId,
roomId: RoomId,
isAudioCall: Boolean,
clientId: String,
languageTag: String?,
theme: String?

View file

@ -33,6 +33,7 @@ fun aCallNotificationData(
timestamp: Long = 0L,
expirationTimestamp: Long = 30_000L,
textContent: String? = null,
audioOnly: Boolean = false,
): CallNotificationData = CallNotificationData(
sessionId = sessionId,
roomId = roomId,
@ -45,4 +46,5 @@ fun aCallNotificationData(
timestamp = timestamp,
expirationTimestamp = expirationTimestamp,
textContent = textContent,
audioOnly = audioOnly
)

View file

@ -3,7 +3,7 @@
<string name="screen_create_room_action_create_room">"Naujas kambarys"</string>
<string name="screen_create_room_add_people_title">"Pakviesti žmonių"</string>
<string name="screen_create_room_error_creating_room">"Kuriant kambarį įvyko klaida"</string>
<string name="screen_create_room_private_option_description">"Į šį kambarį gali patekti tik pakviesti žmonės. Visi pranešimai yra užšifruoti nuo pradžios iki galo."</string>
<string name="screen_create_room_private_option_description">"Tik visapusiškai pakviestieji asmenys gali jungtis."</string>
<string name="screen_create_room_public_option_description">"Bet kas gali rasti šį kambarį.
Tai galite bet kada pakeisti kambario nustatymuose."</string>
<string name="screen_create_room_topic_label">"Tema (nebūtina)"</string>

View file

@ -15,6 +15,7 @@ Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_public_option_title">"Offentlig"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Alle i %1$s kan bli med, mens alle andre må be om tilgang."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Be om å få bli med"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
@ -27,6 +28,7 @@ Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
<string name="screen_create_room_space_selection_no_space_description">"(ingen område)"</string>
<string name="screen_create_room_space_selection_no_space_option">"Skal ikke legges til et område"</string>
<string name="screen_create_room_space_selection_no_space_title">"Ingen områder valgt"</string>
<string name="screen_create_room_space_selection_sheet_title">"Legg til området"</string>
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>

View file

@ -12,5 +12,5 @@
<string name="screen_identity_waiting_on_other_device">"Odotetaan toista laitetta…"</string>
<string name="screen_notification_optin_subtitle">"Voit muuttaa asetuksia myöhemmin."</string>
<string name="screen_notification_optin_title">"Salli ilmoitukset ja älä koskaan missaa viestejä"</string>
<string name="screen_session_verification_enter_recovery_key">"Syötä palautusavain"</string>
<string name="screen_session_verification_enter_recovery_key">"Anna palautusavain"</string>
</resources>

View file

@ -9,7 +9,7 @@
<string name="banner_set_up_recovery_submit">"Ota palautus käyttöön"</string>
<string name="banner_set_up_recovery_title">"Ota palautus käyttöön tilisi suojaamiseksi"</string>
<string name="confirm_recovery_key_banner_message">"Vahvista palautusavaimesi, jotta pääset edelleen käyttämään avainten säilytystä ja viestihistoriaa."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Syötä palautusavaimesi"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Anna palautusavaimesi"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Unohditko palautusavaimesi?"</string>
<string name="confirm_recovery_key_banner_title">"Avainten säilytys ei ole synkronoitu"</string>
<string name="full_screen_intent_banner_message">"Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua."</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Du bannades av %1$s."</string>
<string name="screen_join_room_ban_message">"Du bannades från det här rummet"</string>
<string name="screen_join_room_ban_message">"Du bannades"</string>
<string name="screen_join_room_ban_reason">"Anledning: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Avbryt begäran"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, avbryt"</string>
@ -12,7 +12,7 @@
<string name="screen_join_room_decline_and_block_alert_title">"Avvisa inbjudan och blockera"</string>
<string name="screen_join_room_decline_and_block_button_title">"Avvisa och blockera"</string>
<string name="screen_join_room_fail_message">"Misslyckades att gå med."</string>
<string name="screen_join_room_fail_reason">"Detta rum är antingen endast för inbjudna eller så kan det finnas begränsningar för åtkomst på utrymmesnivå."</string>
<string name="screen_join_room_fail_reason">"Du måste antingen bli inbjuden att gå med eller så kan det finnas åtkomstbegränsningar."</string>
<string name="screen_join_room_forget_action">"Glöm"</string>
<string name="screen_join_room_invite_required_message">"Du behöver en inbjudan för att gå med"</string>
<string name="screen_join_room_join_action">"Gå med"</string>

View file

@ -6,7 +6,7 @@
<string name="screen_link_new_device_desktop_submit">"Valmis skannaamaan"</string>
<string name="screen_link_new_device_desktop_title">"Avaa %1$s pöytätietokoneella saadaksesi QR-koodin"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numerot eivät täsmää"</string>
<string name="screen_link_new_device_enter_number_notice">"Syötä 2-numeroinen koodi"</string>
<string name="screen_link_new_device_enter_number_notice">"Kirjoita 2-numeroinen koodi"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Tämä varmistaa, että yhteys toiseen laitteeseesi on turvallinen."</string>
<string name="screen_link_new_device_enter_number_title">"Kirjoita toisessa laitteessa näkyvä numero"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Palveluntarjoajasi ei tue %1$s -sovellusta"</string>

View file

@ -4,7 +4,7 @@
<string name="screen_link_new_device_desktop_step1">"%1$s uygulamasını bir dizüstü veya masaüstü bilgisayarda açın"</string>
<string name="screen_link_new_device_desktop_step3">"QR kodunu bu cihazla tarayın"</string>
<string name="screen_link_new_device_desktop_submit">"Taramaya hazır"</string>
<string name="screen_link_new_device_desktop_title">"QR kodu almak için %1$s uygulamasını masaüstü bilgisayarda açın%1$s%1$s"</string>
<string name="screen_link_new_device_desktop_title">"QR kodu almak için %1$s uygulamasını masaüstü bilgisayarda açın"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Sayılar uyuşmuyor"</string>
<string name="screen_link_new_device_enter_number_notice">"2 haneli kodu girin"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Bu işlem, diğer cihazla olan bağlantının güvenli olduğunu doğrular."</string>

View file

@ -33,7 +33,7 @@
<string name="screen_login_error_invalid_user_id">"Tämä ei ole kelvollinen käyttäjätunnus. Odotettu muoto: \'@käyttäjä:kotipalvelin.fi\'"</string>
<string name="screen_login_error_refresh_tokens">"Tämä palvelin on määritetty käyttämään refresh tokeneja. Näitä ei tueta salasanapohjaisen kirjautumisen kanssa."</string>
<string name="screen_login_error_unsupported_authentication">"Valitsemasi kotipalvelin ei tue salasana- tai OIDC-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin."</string>
<string name="screen_login_form_header">"Syötä tietosi"</string>
<string name="screen_login_form_header">"Anna tietosi"</string>
<string name="screen_login_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_login_title">"Tervetuloa takaisin!"</string>
<string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</string>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Keisti paskyros teikėją"</string>
<string name="screen_account_provider_form_hint">"Pagrindinio serverio adresas"</string>
<string name="screen_account_provider_form_notice">"Įveskite paieškos terminą arba domeno adresą."</string>
<string name="screen_account_provider_form_subtitle">"Ieškokite bendrovės, bendruomenės arba privataus serverio."</string>
<string name="screen_account_provider_form_title">"Rasti paskyros teikėją"</string>
<string name="screen_account_provider_signin_subtitle">"Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus."</string>

View file

@ -272,10 +272,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
}
override fun navigateToRoomCall(roomId: RoomId) {
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall(
sessionId = sessionId,
roomId = roomId,
isAudioCall = isAudioCall
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType)
@ -488,10 +489,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId))
}
override fun navigateToRoomCall(roomId: RoomId) {
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall(
sessionId = sessionId,
roomId = roomId,
isAudioCall = isAudioCall
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType)

View file

@ -125,7 +125,7 @@ class MessagesNode(
fun navigateToSendLocation()
fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId)
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToRoomDetails()
fun navigateToPinnedMessagesList()
@ -279,7 +279,9 @@ class MessagesNode(
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList,
modifier = modifier,
knockRequestsBannerView = {

View file

@ -130,7 +130,7 @@ fun MessagesView(
onLinkClick: (String, Boolean) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false,
@ -423,7 +423,7 @@ private fun MessagesViewContent(
onMessageLongClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit,
forceJumpToBottomVisibility: Boolean,
onSwipeToReply: (TimelineItem.Event) -> Unit,

View file

@ -130,7 +130,7 @@ class ThreadedMessagesNode(
fun navigateToSendLocation()
fun navigateToCreatePoll()
fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId)
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
}
@ -281,7 +281,9 @@ class ThreadedMessagesNode(
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onJoinCallClick = { isAudioCall ->
callback.navigateToRoomCall(room.roomId, isAudioCall)
},
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},

View file

@ -100,7 +100,7 @@ fun TimelineView(
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
forceJumpToBottomVisibility: Boolean = false,

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
@ -35,7 +36,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun CallMenuItem(
roomCallState: RoomCallState,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
when (roomCallState) {
@ -52,7 +53,7 @@ internal fun CallMenuItem(
is RoomCallState.OnGoing -> {
OnGoingCallMenuItem(
roomCallState = roomCallState,
onJoinCallClick = onJoinCallClick,
onJoinCallClick = { onJoinCallClick(roomCallState.isAudioCall) },
modifier = modifier,
)
}
@ -62,18 +63,31 @@ internal fun CallMenuItem(
@Composable
private fun StandByCallMenuItem(
roomCallState: RoomCallState.StandBy,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(
modifier = modifier,
onClick = onJoinCallClick,
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
Row(modifier = modifier) {
// Only show voice call in DMs
if (roomCallState.isDM) {
IconButton(
onClick = { onJoinCallClick(true) },
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VoiceCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_voice_call),
)
}
}
IconButton(
onClick = { onJoinCallClick(false) },
enabled = roomCallState.canStartCall,
) {
Icon(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
}
}
@ -96,7 +110,11 @@ private fun OnGoingCallMenuItem(
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(),
imageVector = if (roomCallState.isAudioCall) {
CompoundIcons.VoiceCallSolid()
} else {
CompoundIcons.VideoCallSolid()
},
contentDescription = null
)
Spacer(Modifier.width(8.dp))

View file

@ -46,7 +46,7 @@ internal fun TimelineItemCallNotifyView(
event: TimelineItem.Event,
roomCallState: RoomCallState,
onLongClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(

View file

@ -72,7 +72,7 @@ internal fun TimelineItemRow(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
eventSink: (TimelineEvent.TimelineItemEvent) -> Unit,
modifier: Modifier = Modifier,
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =

View file

@ -66,7 +66,7 @@ internal fun MessagesViewTopBar(
dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon,
onRoomDetailsClick: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {

View file

@ -19,8 +19,13 @@
<string name="screen_room_attachment_source_camera_video">"Įrašyti vaizdo įrašą"</string>
<string name="screen_room_attachment_source_files">"Priedas"</string>
<string name="screen_room_attachment_source_gallery">"Nuotraukų ir vaizdo įrašų biblioteka"</string>
<string name="screen_room_invite_again_alert_message">"Ar norėtumėte juos pakviesti atgal?"</string>
<string name="screen_room_invite_again_alert_title">"Šiame pokalbyje esate vieni."</string>
<string name="screen_room_retry_send_menu_send_again_action">"Siųsti vėl"</string>
<string name="screen_room_retry_send_menu_title">"Jūsų žinutė nepavyko išsiųsti."</string>
<string name="screen_room_timeline_beginning_of_room">"Tai yra %1$s pradžia."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Tai yra šio pokalbio pradžia."</string>
<string name="screen_room_timeline_no_permission_to_post">"Neturite leidimą skelbti šiame kambaryje."</string>
<string name="screen_room_timeline_read_marker_title">"Naujų"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d kambario pakeitimas"</item>

View file

@ -53,6 +53,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
@ -71,6 +72,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf
@ -122,7 +124,7 @@ class MessagesViewTest {
val state = aMessagesState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView(
state = state,
onJoinCallClick = callback,
@ -132,6 +134,23 @@ class MessagesViewTest {
}
}
@Test
fun `clicking on join voice call invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder,
roomCallState = aStandByCallState(isDM = true)
)
ensureCalledOnceWithParam(true) { callback ->
rule.setMessagesView(
state = state,
onJoinCallClick = callback,
)
val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call)
rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
}
}
@Test
fun `clicking on an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
@ -609,7 +628,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
) {
setSafeContent {

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder
@ -186,7 +185,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
forceJumpToBottomVisibility: Boolean = false,
) {
setSafeContent(clearAndroidUiDispatcher = true) {

View file

@ -24,5 +24,11 @@ interface NetworkMonitor {
/**
* Checks if the active network is being blocked by Doze, even if it's available.
*/
fun isNetworkBlocked(): Boolean
val isNetworkBlocked: StateFlow<Boolean>
/**
* A flow indicating whether the app is running in an air-gapped environment.
* An air-gapped environment is an environment that is not connected to the internet, and where the app can only communicate with a limited set of servers.
*/
val isInAirGappedEnvironment: StateFlow<Boolean>
}

View file

@ -1,4 +1,5 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
@ -23,4 +24,8 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.features.networkmonitor.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.networkmonitor.test)
}

View file

@ -13,18 +13,21 @@ package io.element.android.features.networkmonitor.impl
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@ -39,13 +42,13 @@ import java.util.concurrent.atomic.AtomicInteger
@SingleIn(AppScope::class)
class DefaultNetworkMonitor(
@ApplicationContext context: Context,
@AppCoroutineScope
appCoroutineScope: CoroutineScope,
@AppCoroutineScope appCoroutineScope: CoroutineScope,
private val buildMeta: BuildMeta,
) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val blockedNetworkBlockedChecker = NetworkBlockedChecker(connectivityManager)
override fun isNetworkBlocked(): Boolean = blockedNetworkBlockedChecker.isNetworkBlocked()
override val isNetworkBlocked = MutableStateFlow(NetworkBlockedChecker(connectivityManager).isNetworkBlocked())
override val isInAirGappedEnvironment = MutableStateFlow(false)
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
@ -63,6 +66,27 @@ class DefaultNetworkMonitor(
}
}
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Timber.d("Network ${network.networkHandle} blocked status changed: $blocked.")
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
// If the network is blocked, it means that Doze is preventing the app from using the network, even if it's available.
isNetworkBlocked.value = blocked
}
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
if (!buildMeta.isEnterpriseBuild) {
// The air-gapped environment detection is only relevant for the enterprise build.
return
}
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
// (according to Google), which is a common case in air-gapped environments.
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
}
override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Connected)

View file

@ -14,10 +14,10 @@ import android.net.ConnectivityManager
import android.net.NetworkInfo
/**
* Helper to check if the active network in [ConnectivityManager] is blocked.
* Helper to synchronously check if the active network in [ConnectivityManager] is blocked.
*
* This is extracted to its own class because it uses deprecated APIs (but the only ones that are reliable)
* and we don't want to suppress deprecations everywhere.
* and we don't want to suppress deprecations everywhere in the file this would be called.
*/
class NetworkBlockedChecker(
private val connectivityManager: ConnectivityManager,

View file

@ -14,8 +14,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor(
initialStatus: NetworkStatus = NetworkStatus.Connected,
private val isNetworkBlockedLambda: () -> Boolean = { false },
) : NetworkMonitor {
override val connectivity = MutableStateFlow(initialStatus)
override fun isNetworkBlocked(): Boolean = isNetworkBlockedLambda()
override val isNetworkBlocked = MutableStateFlow(false)
override val isInAirGappedEnvironment = MutableStateFlow(false)
fun givenNetworkBlocked(isBlocked: Boolean) {
isNetworkBlocked.value = isBlocked
}
fun givenIsInAirGappedEnvironment(isInAirGapped: Boolean) {
isInAirGappedEnvironment.value = isInAirGapped
}
}

View file

@ -9,6 +9,8 @@
package io.element.android.features.rageshake.impl.crash
import android.os.Build
import android.os.TransactionTooLargeException
import io.element.android.libraries.architecture.appyx.lastCapturedNavState
import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber
import java.io.PrintWriter
@ -61,6 +63,13 @@ class VectorUncaughtExceptionHandler(
val sw = StringWriter()
val pw = PrintWriter(sw, true)
throwable.printStackTrace(pw)
if (throwable is RuntimeException && throwable.cause is TransactionTooLargeException) {
pw.append('\n')
pw.append(lastCapturedNavState)
Timber.v(lastCapturedNavState)
}
append(sw.buffer.toString())
}
Timber.e("FATAL EXCEPTION $bugDescription")

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_room_details">"Redaguoti kambarį"</string>
<string name="screen_room_change_permissions_room_details">"Redaguoti informaciją"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d asmuo"</item>
<item quantity="few">"%1$d asmenys"</item>

View file

@ -18,10 +18,12 @@ sealed interface RoomCallState {
data class StandBy(
val canStartCall: Boolean,
val isDM: Boolean,
) : RoomCallState
data class OnGoing(
val canJoinCall: Boolean,
val isAudioCall: Boolean,
val isUserInTheCall: Boolean,
val isUserLocallyInTheCall: Boolean,
) : RoomCallState

View file

@ -14,9 +14,11 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
override val values: Sequence<RoomCallState> = sequenceOf(
aStandByCallState(),
aStandByCallState(canStartCall = false),
aStandByCallState(canStartCall = false, isDM = true),
anOngoingCallState(),
anOngoingCallState(canJoinCall = false),
anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
anOngoingCallState(canJoinCall = true, isAudioCall = true),
RoomCallState.Unavailable,
)
}
@ -25,14 +27,18 @@ fun anOngoingCallState(
canJoinCall: Boolean = true,
isUserInTheCall: Boolean = false,
isUserLocallyInTheCall: Boolean = isUserInTheCall,
isAudioCall: Boolean = false,
) = RoomCallState.OnGoing(
canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall,
isAudioCall = isAudioCall
)
fun aStandByCallState(
canStartCall: Boolean = true,
isDM: Boolean = false,
) = RoomCallState.StandBy(
canStartCall = canStartCall,
isDM
)

View file

@ -21,6 +21,7 @@ import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@ -56,8 +57,13 @@ class RoomCallStatePresenter(
canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall,
// TODO resolve intent while the call is ongoing
isAudioCall = false
)
else -> RoomCallState.StandBy(
canStartCall = canJoinCall,
isDM = roomInfo.isDm
)
else -> RoomCallState.StandBy(canStartCall = canJoinCall)
}
}
}

View file

@ -41,6 +41,7 @@ class RoomCallStatePresenterTest {
assertThat(initialState).isEqualTo(
RoomCallState.StandBy(
canStartCall = false,
isDM = false
)
)
}
@ -79,6 +80,28 @@ class RoomCallStatePresenterTest {
assertThat(initialState).isEqualTo(
RoomCallState.StandBy(
canStartCall = true,
isDM = false
)
)
}
}
@Test
fun `present - initial state - when is direct room`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDirect = true),
roomPermissions = roomPermissions(true),
)
)
val presenter = createRoomCallStatePresenter(joinedRoom = room)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
RoomCallState.StandBy(
canStartCall = true,
isDM = true
)
)
}
@ -98,6 +121,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = false,
isAudioCall = false,
isUserInTheCall = false,
isUserLocallyInTheCall = false,
)
@ -125,6 +149,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true,
isUserLocallyInTheCall = false,
)
@ -155,6 +180,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true,
isUserLocallyInTheCall = true,
)
@ -187,6 +213,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true,
isUserLocallyInTheCall = true,
)
@ -195,6 +222,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true,
isUserLocallyInTheCall = false,
)
@ -208,6 +236,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = false,
isUserLocallyInTheCall = false,
)
@ -221,6 +250,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo(
RoomCallState.StandBy(
canStartCall = true,
isDM = false
)
)
}

View file

@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.verification.VerificationRequest
@ -223,10 +224,11 @@ class RoomDetailsFlowNode(
backstack.push(NavTarget.RoomMemberDetails(userId))
}
override fun navigateToRoomCall() {
override fun navigateToRoomCall(callIntent: CallIntent) {
val inputs = CallType.RoomCall(
sessionId = room.sessionId,
roomId = room.roomId,
isAudioCall = callIntent == CallIntent.AUDIO
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(inputs)
@ -284,8 +286,14 @@ class RoomDetailsFlowNode(
callback.navigateToRoom(roomId, emptyList())
}
override fun startCall(dmRoomId: RoomId) {
elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId))
override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(
CallType.RoomCall(
roomId = dmRoomId,
sessionId = room.sessionId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
}
override fun startVerifyUserFlow(userId: UserId) {

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
@ -59,7 +60,7 @@ class RoomDetailsNode(
fun navigateToKnockRequestsList()
fun navigateToSecurityAndPrivacy()
fun navigateToRoomMemberDetails(userId: UserId)
fun navigateToRoomCall()
fun navigateToRoomCall(callIntent: CallIntent)
fun navigateToReportRoom()
fun navigateToSelectNewOwnersWhenLeaving()
}

View file

@ -79,6 +79,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName
@ -105,7 +106,7 @@ fun RoomDetailsView(
openPollHistory: () -> Unit,
openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit,
onJoinCallClick: (CallIntent) -> Unit,
onPinnedMessagesClick: () -> Unit,
onKnockRequestsClick: () -> Unit,
onSecurityAndPrivacyClick: () -> Unit,
@ -327,7 +328,7 @@ private fun MainActionsSection(
state: RoomDetailsState,
onShareRoom: () -> Unit,
onInvitePeople: () -> Unit,
onCall: () -> Unit,
onCall: (callIntent: CallIntent) -> Unit,
) {
Row(
modifier = Modifier
@ -356,10 +357,19 @@ private fun MainActionsSection(
}
if (state.roomCallState.hasPermissionToJoin()) {
// TODO Improve the view depending on all the cases here?
if (state.roomType is RoomDetailsType.Dm) {
// As per design, only show voice call in DM
MainActionButton(
title = stringResource(CommonStrings.action_call),
imageVector = CompoundIcons.VoiceCall(),
onClick = { onCall(CallIntent.AUDIO) },
)
}
MainActionButton(
title = stringResource(CommonStrings.action_call),
title = stringResource(CommonStrings.common_video),
imageVector = CompoundIcons.VideoCall(),
onClick = onCall,
onClick = { onCall(CallIntent.VIDEO) },
)
}
if (state.roomType is RoomDetailsType.Room) {

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService
@ -67,8 +68,8 @@ class RoomMemberDetailsNode(
callback.navigateToRoom(roomId)
}
fun onStartCall(roomId: RoomId) {
callback.startCall(roomId)
fun onStartCall(roomId: RoomId, callIntent: CallIntent) {
callback.startCall(roomId, callIntent)
}
val state = presenter.present()

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_room_details">"Redaguoti kambarį"</string>
<string name="screen_room_change_permissions_room_details">"Redaguoti informaciją"</string>
<string name="screen_room_details_add_topic_title">"Pridėti temą"</string>
<string name="screen_room_details_edit_room_title">"Redaguoti kambarį"</string>
<string name="screen_room_details_edit_room_title">"Redaguoti informaciją"</string>
<string name="screen_room_details_edition_error">"Įvyko nežinoma klaida ir informacijos pakeisti nepavyko."</string>
<string name="screen_room_details_edition_error_title">"Nepavyko atnaujinti kambario"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Žinutės yra užrakintos. Tik Jūs ir gavėjai turite unikalius raktus joms atrakinti."</string>

View file

@ -50,6 +50,8 @@
<string name="screen_room_details_error_loading_notification_settings">"Ett fel uppstod vid laddning av aviseringsinställningar."</string>
<string name="screen_room_details_error_muting">"Misslyckades att tysta det här rummet, vänligen pröva igen."</string>
<string name="screen_room_details_error_unmuting">"Misslyckades att avtysta det här rummet, vänligen pröva igen."</string>
<string name="screen_room_details_invite_people_dont_close">"Stäng inte appen förrän det är klart."</string>
<string name="screen_room_details_invite_people_preparing">"Förbereder inbjudningar …"</string>
<string name="screen_room_details_invite_people_title">"Bjud in personer"</string>
<string name="screen_room_details_leave_conversation_title">"Lämna konversation"</string>
<string name="screen_room_details_leave_room_title">"Lämna rum"</string>

View file

@ -18,6 +18,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.testtags.TestTags
@ -121,7 +122,25 @@ class RoomDetailsViewTest {
@Test
fun `click on call invokes expected callback`() {
ensureCalledOnce { callback ->
ensureCalledOnceWithParam(CallIntent.AUDIO) { callback ->
rule.setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
canInvite = true,
roomType = RoomDetailsType.Dm(
aRoomMember(UserId("@me:local.org")),
aRoomMember(UserId("@other:local.org"))
),
),
onJoinCallClick = callback,
)
rule.clickOn(CommonStrings.action_call)
}
}
@Test
fun `click on video call invokes expected callback`() {
ensureCalledOnceWithParam(CallIntent.VIDEO) { callback ->
rule.setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
@ -129,7 +148,7 @@ class RoomDetailsViewTest {
),
onJoinCallClick = callback,
)
rule.clickOn(CommonStrings.action_call)
rule.clickOn(CommonStrings.common_video)
}
}
@ -343,7 +362,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
openPollHistory: () -> Unit = EnsureNeverCalled(),
openMediaGallery: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: (CallIntent) -> Unit = EnsureNeverCalledWithParam(),
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_details_edit_room_title">"Redaguoti kambarį"</string>
<string name="screen_room_details_edit_room_title">"Redaguoti informaciją"</string>
<string name="screen_room_details_edition_error">"Įvyko nežinoma klaida ir informacijos pakeisti nepavyko."</string>
<string name="screen_room_details_edition_error_title">"Nepavyko atnaujinti kambario"</string>
<string name="screen_room_details_updating_room">"Atnaujinamas kambarys…"</string>

View file

@ -9,7 +9,7 @@
<string name="screen_chat_backup_key_storage_toggle_title">"Salli avainten säilytys"</string>
<string name="screen_chat_backup_recovery_action_change">"Vaihda palautusavain"</string>
<string name="screen_chat_backup_recovery_action_change_description">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."</string>
<string name="screen_chat_backup_recovery_action_confirm">"Syötä palautusavain"</string>
<string name="screen_chat_backup_recovery_action_confirm">"Anna palautusavain"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Avainten säilytys ei ole tällä hetkellä synkronoitu."</string>
<string name="screen_chat_backup_recovery_action_setup">"Ota palautus käyttöön"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$s -sovelluksesta kaikkialla."</string>
@ -43,10 +43,10 @@
<string name="screen_recovery_key_confirm_error_content">"Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen."</string>
<string name="screen_recovery_key_confirm_error_title">"Väärä palautusavain"</string>
<string name="screen_recovery_key_confirm_key_description">"Jos sinulla on turva-avain tai turvalause, sekin toimii."</string>
<string name="screen_recovery_key_confirm_key_placeholder">"Syötä…"</string>
<string name="screen_recovery_key_confirm_key_placeholder">"Kirjoita…"</string>
<string name="screen_recovery_key_confirm_lost_recovery_key">"Hukkasitko palautusavaimesi?"</string>
<string name="screen_recovery_key_confirm_success">"Palautusavain vahvistettu"</string>
<string name="screen_recovery_key_confirm_title">"Syötä palautusavaimesi"</string>
<string name="screen_recovery_key_confirm_title">"Anna palautusavaimesi"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Palautusavain kopioitu"</string>
<string name="screen_recovery_key_generating_key">"Luodaan…"</string>
<string name="screen_recovery_key_save_action">"Tallenna palautusavain"</string>
@ -64,7 +64,7 @@
<string name="screen_reset_encryption_confirmation_alert_subtitle">"Tätä prosessia ei voi peruuttaa."</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Haluatko varmasti nollata identiteettisi?"</string>
<string name="screen_reset_encryption_password_error">"Tapahtui tuntematon virhe. Tarkista, että tilisi salasana on oikein ja yritä uudelleen."</string>
<string name="screen_reset_encryption_password_placeholder">"Syötä…"</string>
<string name="screen_reset_encryption_password_placeholder">"Kirjoita…"</string>
<string name="screen_reset_encryption_password_subtitle">"Vahvista, että haluat nollata identiteettisi."</string>
<string name="screen_reset_encryption_password_title">"Kirjoita tilisi salasana jatkaaksesi"</string>
</resources>

View file

@ -8,13 +8,16 @@
</plurals>
<string name="screen_leave_space_subtitle">"Velg rommene du vil forlate, som du ikke er den eneste administratoren for:"</string>
<string name="screen_leave_space_subtitle_last_admin">"Du må tildele en annen administrator for dette området før du kan forlate det."</string>
<string name="screen_leave_space_subtitle_last_owner">"Du er den eneste eieren av%1$s. Du må overføre eierskapet til noen andre før du drar."</string>
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"</string>
<string name="screen_leave_space_title">"Forlat %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Du er den eneste administratoren for %1$s"</string>
<string name="screen_leave_space_title_last_owner">"Overfør eierskap"</string>
<string name="screen_space_add_room_action">"Rom"</string>
<string name="screen_space_add_rooms_room_access_description">"Hvis du legger til et rom, vil det ikke påvirke tilgangen til rommet. For å endre tilgangen, gå til Rominnstillinger &gt; Sikkerhet og personvern."</string>
<string name="screen_space_empty_state_title">"Legg til ditt første rom"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Fjerning av et rom vil ikke påvirke tilgangen til rommet. For å endre tilgangen, gå til Rominformasjon &gt; Personvern og sikkerhet."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Fjern %1$d rom fra %2$s"</item>
<item quantity="other">"Fjern %1$d rommene fra %2$s"</item>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_leave_space_choose_owners_action">"Välj ägare"</string>
<string name="screen_leave_space_title">"Lämna %1$s?"</string>
<string name="screen_space_settings_leave_space">"Lämna utrymmet"</string>
<string name="screen_space_settings_roles_and_permissions">"Roller och behörigheter"</string>
<string name="screen_space_settings_security_and_privacy">"Säkerhet och sekretess"</string>
</resources>

View file

@ -5,7 +5,7 @@
<string name="screen_start_chat_error_starting_chat">"Keskustelun aloituksessa tapahtui virhe"</string>
<string name="screen_start_chat_join_room_by_address_action">"Liity huoneeseen osoitteella"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Osoite ei ole kelvollinen"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Syötä…"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Kirjoita…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Täsmäävä huone löytyi"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Huonetta ei löytynyt"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"esim. #huoneen-nimi:matrix.org"</string>

View file

@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.parcelize.Parcelize
@ -83,8 +84,14 @@ class UserProfileFlowNode(
callback.navigateToRoom(roomId)
}
override fun startCall(dmRoomId: RoomId) {
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId))
override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(
CallType.RoomCall(
sessionId = sessionId,
roomId = dmRoomId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
}
override fun startVerifyUserFlow(userId: UserId) {

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