Merge branch 'release/26.03.3'

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,8 @@ on:
pull_request: pull_request:
types: [ labeled ] types: [ labeled ]
permissions: {}
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 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: with:
persist-credentials: false persist-credentials: false
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@v5 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
# Add gradle cache, this should speed up the process # Add gradle cache, this should speed up the process
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,8 @@ on:
# At 00:00 on every Monday UTC # At 00:00 on every Monday UTC
- cron: '0 0 * * 1' - cron: '0 0 * * 1'
permissions: {}
jobs: jobs:
sync-sas-strings: sync-sas-strings:
runs-on: ubuntu-latest 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' }} 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. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v6 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Install Prerequisite dependencies - name: Install Prerequisite dependencies

View file

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

View file

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

View file

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

View file

@ -2,6 +2,8 @@ name: Validate Git LFS
on: [pull_request, merge_group] on: [pull_request, merge_group]
permissions: {}
jobs: jobs:
build: build:
runs-on: ubuntu-latest 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 Changes in Element X v26.03.0
============================= =============================

View file

@ -16,7 +16,7 @@
* [Code quality](#code-quality) * [Code quality](#code-quality)
* [detekt](#detekt) * [detekt](#detekt)
* [ktlint](#ktlint) * [ktlint](#ktlint)
* [knit](#knit) * [checkDocs](#checkdocs)
* [lint](#lint) * [lint](#lint)
* [Unit tests](#unit-tests) * [Unit tests](#unit-tests)
* [konsist](#konsist) * [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) 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 So everytime the toc should be updated, just run
<pre> <pre>
./gradlew knit ./gradlew generateDocsToc
</pre> </pre>
and commit the changes. 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 The CI will check that markdown files are up to date by running
<pre> <pre>
./gradlew knitCheck ./gradlew checkDocs
</pre> </pre>
#### lint #### lint

View file

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

View file

@ -26,7 +26,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.compound.colors.SemanticColorsLightDark 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.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag 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.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.designsystem.theme.ElementThemeApp
@ -100,7 +100,9 @@ class MainActivity : NodeActivity() {
@Composable @Composable
private fun MainNodeHost() { 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( MainNode(
it, it,
plugins = listOf( plugins = listOf(
@ -110,7 +112,7 @@ class MainActivity : NodeActivity() {
mainNode = node mainNode = node
mainNode.handleIntent(intent) mainNode.handleIntent(intent)
} }
} },
), ),
context = applicationContext context = applicationContext
) )

View file

@ -175,12 +175,23 @@ tasks.register("runQualityChecks") {
tasks.findByName("ktlintCheck")?.let { dependsOn(it) } tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
// tasks.findByName("buildHealth")?.let { dependsOn(it) } // tasks.findByName("buildHealth")?.let { dependsOn(it) }
} }
dependsOn(":app:knitCheck") dependsOn("checkDocs")
// Make sure all checks run even if some fail // Make sure all checks run even if some fail
gradle.startParameter.isContinueOnFailure = true 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 // Make sure to delete old screenshots before recording new ones
subprojects { subprojects {
val snapshotsDir = File("${project.projectDir}/src/test/snapshots") 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) * [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) * [Installing from the App Bundle](#installing-from-the-app-bundle)
* [Requirements](#requirements) * [Requirements](#requirements)
* [Steps](#steps) * [Steps](#steps-1)
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone) * [I already have the application on my phone](#i-already-have-the-application-on-my-phone-1)
<!--- END --> <!--- END -->

View file

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

View file

@ -8,7 +8,7 @@
* [Stop Synapse](#stop-synapse) * [Stop Synapse](#stop-synapse)
* [Troubleshoot](#troubleshoot) * [Troubleshoot](#troubleshoot)
* [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver) * [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) * [virtualenv command fails](#virtualenv-command-fails)
<!--- END --> <!--- END -->

View file

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

View file

@ -3,23 +3,23 @@
<!--- TOC --> <!--- TOC -->
* [Introduction](#introduction) * [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) * [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) * [Humans](#humans)
* [Draft PR?](#draft-pr?) * [Draft PR?](#draft-pr)
* [Base branch](#base-branch) * [Base branch](#base-branch)
* [PR Review Assignment](#pr-review-assignment) * [PR Review Assignment](#pr-review-assignment)
* [PR review time](#pr-review-time) * [PR review time](#pr-review-time)
* [Re-request PR review](#re-request-pr-review) * [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) * [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr)
* [Bots](#bots) * [Bots](#bots)
* [Renovate](#renovate) * [Renovate](#renovate)
* [Gradle wrapper](#gradle-wrapper) * [Gradle wrapper](#gradle-wrapper)
* [Sync analytics plan](#sync-analytics-plan) * [Sync analytics plan](#sync-analytics-plan)
* [Reviewing PR](#reviewing-pr) * [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) * [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr)
* [Rules](#rules) * [Rules](#rules)
* [Check the form](#check-the-form) * [Check the form](#check-the-form)
@ -29,7 +29,7 @@
* [Check the commit](#check-the-commit) * [Check the commit](#check-the-commit)
* [Check the substance](#check-the-substance) * [Check the substance](#check-the-substance)
* [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr) * [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) * [Merge conflict](#merge-conflict)
* [When and who can merge PR](#when-and-who-can-merge-pr) * [When and who can merge PR](#when-and-who-can-merge-pr)
* [Merge type](#merge-type) * [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( data class RoomCall(
val sessionId: SessionId, val sessionId: SessionId,
val roomId: RoomId, val roomId: RoomId,
val isAudioCall: Boolean
) : CallType { ) : CallType {
override fun toString(): String { 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, expirationTimestamp = expirationTimestamp,
notificationChannelId = notificationChannelId, notificationChannelId = notificationChannelId,
textContent = textContent, textContent = textContent,
audioOnly = callType.isAudioCall
) )
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
} }

View file

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

View file

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

View file

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

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, sessionId = inputs.sessionId,
roomId = inputs.roomId, roomId = inputs.roomId,
clientId = UUID.randomUUID().toString(), clientId = UUID.randomUUID().toString(),
isAudioCall = inputs.isAudioCall,
languageTag = languageTag, languageTag = languageTag,
theme = theme, theme = theme,
).getOrThrow() ).getOrThrow()

View file

@ -112,7 +112,13 @@ class IncomingCallActivity : AppCompatActivity() {
} }
private fun onAnswer(notificationData: CallNotificationData) { 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() { 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme 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.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text 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 import io.element.android.libraries.ui.strings.CommonStrings
/** /**
@ -103,7 +100,7 @@ internal fun IncomingCallScreen(
ActionButton( ActionButton(
size = 64.dp, size = 64.dp,
onClick = { onAnswer(notificationData) }, onClick = { onAnswer(notificationData) },
icon = CompoundIcons.VoiceCallSolid(), icon = if (notificationData.audioOnly) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
title = stringResource(CommonStrings.action_accept), title = stringResource(CommonStrings.action_accept),
backgroundColor = ElementTheme.colors.iconSuccessPrimary, backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle borderColor = ElementTheme.colors.borderSuccessSubtle
@ -163,21 +160,11 @@ private fun ActionButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun IncomingCallScreenPreview() = ElementPreview { internal fun IncomingCallScreenPreview(
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
) = ElementPreview {
IncomingCallScreen( IncomingCallScreen(
notificationData = CallNotificationData( notificationData = state,
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,
),
onAnswer = {}, onAnswer = {},
onCancel = {}, onCancel = {},
) )

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest {
@Test @Test
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
val entryPoint = createEntryPoint() 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 expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java)
val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
@ -53,7 +53,7 @@ class DefaultElementCallEntryPointTest {
val entryPoint = createEntryPoint(activeCallManager = activeCallManager) val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
entryPoint.handleIncomingCall( 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, eventId = AN_EVENT_ID,
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
roomName = "roomName", roomName = "roomName",

View file

@ -65,7 +65,33 @@ class RingingCallNotificationCreatorTest {
getUserIconLambda.assertions().isCalledOnce() 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, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -77,6 +103,7 @@ class RingingCallNotificationCreatorTest {
timestamp = 0L, timestamp = 0L,
expirationTimestamp = 20L, expirationTimestamp = 20L,
textContent = "textContent", textContent = "textContent",
audioOnly = audioOnly
) )
private fun createRingingCallNotificationCreator( private fun createRingingCallNotificationCreator(

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class DefaultCallWidgetProviderTest {
@Test @Test
fun `getWidget - fails if the session does not exist`() = runTest { fun `getWidget - fails if the session does not exist`() = runTest {
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) }) 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 @Test
@ -40,7 +40,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, null) givenGetRoomResult(A_ROOM_ID, null)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) 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 @Test
@ -52,7 +52,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) 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 @Test
@ -65,7 +65,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) 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 @Test
@ -78,7 +78,7 @@ class DefaultCallWidgetProviderTest {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) 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 @Test
@ -101,7 +101,7 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
activeRoomsHolder = activeRoomsHolder 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 @Test
@ -122,7 +122,7 @@ class DefaultCallWidgetProviderTest {
callWidgetSettingsProvider = settingsProvider, callWidgetSettingsProvider = settingsProvider,
appPreferencesStore = preferencesStore, 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") assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io")
} }

View file

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

View file

@ -33,6 +33,7 @@ fun aCallNotificationData(
timestamp: Long = 0L, timestamp: Long = 0L,
expirationTimestamp: Long = 30_000L, expirationTimestamp: Long = 30_000L,
textContent: String? = null, textContent: String? = null,
audioOnly: Boolean = false,
): CallNotificationData = CallNotificationData( ): CallNotificationData = CallNotificationData(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -45,4 +46,5 @@ fun aCallNotificationData(
timestamp = timestamp, timestamp = timestamp,
expirationTimestamp = expirationTimestamp, expirationTimestamp = expirationTimestamp,
textContent = textContent, 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_action_create_room">"Naujas kambarys"</string>
<string name="screen_create_room_add_people_title">"Pakviesti žmonių"</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_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į. <string name="screen_create_room_public_option_description">"Bet kas gali rasti šį kambarį.
Tai galite bet kada pakeisti kambario nustatymuose."</string> Tai galite bet kada pakeisti kambario nustatymuose."</string>
<string name="screen_create_room_topic_label">"Tema (nebūtina)"</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_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_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_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_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_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</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_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</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_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_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_space_selection_sheet_title">"Legg til området"</string>
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</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_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_subtitle">"Voit muuttaa asetuksia myöhemmin."</string>
<string name="screen_notification_optin_title">"Salli ilmoitukset ja älä koskaan missaa viestejä"</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> </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_submit">"Ota palautus käyttöön"</string>
<string name="banner_set_up_recovery_title">"Ota palautus käyttöön tilisi suojaamiseksi"</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_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_secondary_button_title">"Unohditko palautusavaimesi?"</string>
<string name="confirm_recovery_key_banner_title">"Avainten säilytys ei ole synkronoitu"</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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Du bannades av %1$s."</string> <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_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_action">"Avbryt begäran"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, avbryt"</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_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_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_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_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_invite_required_message">"Du behöver en inbjudan för att gå med"</string>
<string name="screen_join_room_join_action">"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_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_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_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_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_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> <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_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_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_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_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_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> <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_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_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_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_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_login_title">"Tervetuloa takaisin!"</string> <string name="screen_login_title">"Tervetuloa takaisin!"</string>
<string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Keisti paskyros teikėją"</string> <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_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_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> <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)) backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
} }
override fun navigateToRoomCall(roomId: RoomId) { override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
val callType = CallType.RoomCall( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
isAudioCall = isAudioCall
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)
@ -488,10 +489,11 @@ class MessagesFlowNode(
backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId)) 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( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
isAudioCall = isAudioCall
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)

View file

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

View file

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

View file

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

View file

@ -100,7 +100,7 @@ fun TimelineView(
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
forceJumpToBottomVisibility: Boolean = false, 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.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -35,7 +36,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
internal fun CallMenuItem( internal fun CallMenuItem(
roomCallState: RoomCallState, roomCallState: RoomCallState,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (roomCallState) { when (roomCallState) {
@ -52,7 +53,7 @@ internal fun CallMenuItem(
is RoomCallState.OnGoing -> { is RoomCallState.OnGoing -> {
OnGoingCallMenuItem( OnGoingCallMenuItem(
roomCallState = roomCallState, roomCallState = roomCallState,
onJoinCallClick = onJoinCallClick, onJoinCallClick = { onJoinCallClick(roomCallState.isAudioCall) },
modifier = modifier, modifier = modifier,
) )
} }
@ -62,18 +63,31 @@ internal fun CallMenuItem(
@Composable @Composable
private fun StandByCallMenuItem( private fun StandByCallMenuItem(
roomCallState: RoomCallState.StandBy, roomCallState: RoomCallState.StandBy,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconButton( Row(modifier = modifier) {
modifier = modifier, // Only show voice call in DMs
onClick = onJoinCallClick, if (roomCallState.isDM) {
enabled = roomCallState.canStartCall, IconButton(
) { onClick = { onJoinCallClick(true) },
Icon( enabled = roomCallState.canStartCall,
imageVector = CompoundIcons.VideoCallSolid(), ) {
contentDescription = stringResource(CommonStrings.a11y_start_call), 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( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid(), imageVector = if (roomCallState.isAudioCall) {
CompoundIcons.VoiceCallSolid()
} else {
CompoundIcons.VideoCallSolid()
},
contentDescription = null contentDescription = null
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))

View file

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

View file

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

View file

@ -66,7 +66,7 @@ internal fun MessagesViewTopBar(
dmUserIdentityState: IdentityState?, dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon, sharedHistoryIcon: SharedHistoryIcon,
onRoomDetailsClick: () -> Unit, onRoomDetailsClick: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, 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_camera_video">"Įrašyti vaizdo įrašą"</string>
<string name="screen_room_attachment_source_files">"Priedas"</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_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">"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_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> <string name="screen_room_timeline_read_marker_title">"Naujų"</string>
<plurals name="screen_room_timeline_state_changes"> <plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d kambario pakeitimas"</item> <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.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
import io.element.android.features.messages.impl.timeline.model.TimelineItem 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.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.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom 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.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce 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.pressBack
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -122,7 +124,7 @@ class MessagesViewTest {
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView( rule.setMessagesView(
state = state, state = state,
onJoinCallClick = callback, 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 @Test
fun `clicking on an Event invoke expected callback`() { fun `clicking on an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
@ -609,7 +628,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(), onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(), onSendLocationClick: () -> Unit = EnsureNeverCalled(),
onCreatePollClick: () -> Unit = EnsureNeverCalled(), onCreatePollClick: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setSafeContent { 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.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings 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.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder 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(), onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,
) { ) {
setSafeContent(clearAndroidUiDispatcher = true) { 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. * 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.setupDependencyInjection
import extension.testCommonDependencies
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2025 Element Creations Ltd.
@ -23,4 +24,8 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.di) implementation(projects.libraries.di)
api(projects.features.networkmonitor.api) 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.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@ -39,13 +42,13 @@ import java.util.concurrent.atomic.AtomicInteger
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class DefaultNetworkMonitor( class DefaultNetworkMonitor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@AppCoroutineScope @AppCoroutineScope appCoroutineScope: CoroutineScope,
appCoroutineScope: CoroutineScope, private val buildMeta: BuildMeta,
) : NetworkMonitor { ) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) 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 { 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) { override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) { if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Connected) trySendBlocking(NetworkStatus.Connected)

View file

@ -14,10 +14,10 @@ import android.net.ConnectivityManager
import android.net.NetworkInfo 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) * 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( class NetworkBlockedChecker(
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,

View file

@ -14,8 +14,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor( class FakeNetworkMonitor(
initialStatus: NetworkStatus = NetworkStatus.Connected, initialStatus: NetworkStatus = NetworkStatus.Connected,
private val isNetworkBlockedLambda: () -> Boolean = { false },
) : NetworkMonitor { ) : NetworkMonitor {
override val connectivity = MutableStateFlow(initialStatus) 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 package io.element.android.features.rageshake.impl.crash
import android.os.Build 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 io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber import timber.log.Timber
import java.io.PrintWriter import java.io.PrintWriter
@ -61,6 +63,13 @@ class VectorUncaughtExceptionHandler(
val sw = StringWriter() val sw = StringWriter()
val pw = PrintWriter(sw, true) val pw = PrintWriter(sw, true)
throwable.printStackTrace(pw) throwable.printStackTrace(pw)
if (throwable is RuntimeException && throwable.cause is TransactionTooLargeException) {
pw.append('\n')
pw.append(lastCapturedNavState)
Timber.v(lastCapturedNavState)
}
append(sw.buffer.toString()) append(sw.buffer.toString())
} }
Timber.e("FATAL EXCEPTION $bugDescription") Timber.e("FATAL EXCEPTION $bugDescription")

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d asmuo"</item> <item quantity="one">"%1$d asmuo"</item>
<item quantity="few">"%1$d asmenys"</item> <item quantity="few">"%1$d asmenys"</item>

View file

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

View file

@ -14,9 +14,11 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
override val values: Sequence<RoomCallState> = sequenceOf( override val values: Sequence<RoomCallState> = sequenceOf(
aStandByCallState(), aStandByCallState(),
aStandByCallState(canStartCall = false), aStandByCallState(canStartCall = false),
aStandByCallState(canStartCall = false, isDM = true),
anOngoingCallState(), anOngoingCallState(),
anOngoingCallState(canJoinCall = false), anOngoingCallState(canJoinCall = false),
anOngoingCallState(canJoinCall = true, isUserInTheCall = true), anOngoingCallState(canJoinCall = true, isUserInTheCall = true),
anOngoingCallState(canJoinCall = true, isAudioCall = true),
RoomCallState.Unavailable, RoomCallState.Unavailable,
) )
} }
@ -25,14 +27,18 @@ fun anOngoingCallState(
canJoinCall: Boolean = true, canJoinCall: Boolean = true,
isUserInTheCall: Boolean = false, isUserInTheCall: Boolean = false,
isUserLocallyInTheCall: Boolean = isUserInTheCall, isUserLocallyInTheCall: Boolean = isUserInTheCall,
isAudioCall: Boolean = false,
) = RoomCallState.OnGoing( ) = RoomCallState.OnGoing(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall,
isAudioCall = isAudioCall
) )
fun aStandByCallState( fun aStandByCallState(
canStartCall: Boolean = true, canStartCall: Boolean = true,
isDM: Boolean = false,
) = RoomCallState.StandBy( ) = RoomCallState.StandBy(
canStartCall = canStartCall, 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.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom 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.canCall
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
@ -56,8 +57,13 @@ class RoomCallStatePresenter(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall, 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( assertThat(initialState).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = false, canStartCall = false,
isDM = false
) )
) )
} }
@ -79,6 +80,28 @@ class RoomCallStatePresenterTest {
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = true, 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( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = false, canJoinCall = false,
isAudioCall = false,
isUserInTheCall = false, isUserInTheCall = false,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -125,6 +149,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -155,6 +180,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = true, isUserLocallyInTheCall = true,
) )
@ -187,6 +213,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = true, isUserLocallyInTheCall = true,
) )
@ -195,6 +222,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = true, isUserInTheCall = true,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -208,6 +236,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing( RoomCallState.OnGoing(
canJoinCall = true, canJoinCall = true,
isAudioCall = false,
isUserInTheCall = false, isUserInTheCall = false,
isUserLocallyInTheCall = false, isUserLocallyInTheCall = false,
) )
@ -221,6 +250,7 @@ class RoomCallStatePresenterTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
RoomCallState.StandBy( RoomCallState.StandBy(
canStartCall = true, 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.RoomId
import io.element.android.libraries.matrix.api.core.UserId 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.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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
@ -223,10 +224,11 @@ class RoomDetailsFlowNode(
backstack.push(NavTarget.RoomMemberDetails(userId)) backstack.push(NavTarget.RoomMemberDetails(userId))
} }
override fun navigateToRoomCall() { override fun navigateToRoomCall(callIntent: CallIntent) {
val inputs = CallType.RoomCall( val inputs = CallType.RoomCall(
sessionId = room.sessionId, sessionId = room.sessionId,
roomId = room.roomId, roomId = room.roomId,
isAudioCall = callIntent == CallIntent.AUDIO
) )
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(inputs) elementCallEntryPoint.startCall(inputs)
@ -284,8 +286,14 @@ class RoomDetailsFlowNode(
callback.navigateToRoom(roomId, emptyList()) callback.navigateToRoom(roomId, emptyList())
} }
override fun startCall(dmRoomId: RoomId) { override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId)) elementCallEntryPoint.startCall(
CallType.RoomCall(
roomId = dmRoomId,
sessionId = room.sessionId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
} }
override fun startVerifyUserFlow(userId: UserId) { 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.architecture.callback
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId 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.libraries.matrix.api.room.BaseRoom
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -59,7 +60,7 @@ class RoomDetailsNode(
fun navigateToKnockRequestsList() fun navigateToKnockRequestsList()
fun navigateToSecurityAndPrivacy() fun navigateToSecurityAndPrivacy()
fun navigateToRoomMemberDetails(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun navigateToRoomCall() fun navigateToRoomCall(callIntent: CallIntent)
fun navigateToReportRoom() fun navigateToReportRoom()
fun navigateToSelectNewOwnersWhenLeaving() 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.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId 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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
@ -105,7 +106,7 @@ fun RoomDetailsView(
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
openMediaGallery: () -> Unit, openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit, openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit, onJoinCallClick: (CallIntent) -> Unit,
onPinnedMessagesClick: () -> Unit, onPinnedMessagesClick: () -> Unit,
onKnockRequestsClick: () -> Unit, onKnockRequestsClick: () -> Unit,
onSecurityAndPrivacyClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit,
@ -327,7 +328,7 @@ private fun MainActionsSection(
state: RoomDetailsState, state: RoomDetailsState,
onShareRoom: () -> Unit, onShareRoom: () -> Unit,
onInvitePeople: () -> Unit, onInvitePeople: () -> Unit,
onCall: () -> Unit, onCall: (callIntent: CallIntent) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -356,10 +357,19 @@ private fun MainActionsSection(
} }
if (state.roomCallState.hasPermissionToJoin()) { if (state.roomCallState.hasPermissionToJoin()) {
// TODO Improve the view depending on all the cases here? // 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( MainActionButton(
title = stringResource(CommonStrings.action_call), title = stringResource(CommonStrings.common_video),
imageVector = CompoundIcons.VideoCall(), imageVector = CompoundIcons.VideoCall(),
onClick = onCall, onClick = { onCall(CallIntent.VIDEO) },
) )
} }
if (state.roomType is RoomDetailsType.Room) { 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.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId 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.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -67,8 +68,8 @@ class RoomMemberDetailsNode(
callback.navigateToRoom(roomId) callback.navigateToRoom(roomId)
} }
fun onStartCall(roomId: RoomId) { fun onStartCall(roomId: RoomId, callIntent: CallIntent) {
callback.startCall(roomId) callback.startCall(roomId, callIntent)
} }
val state = presenter.present() val state = presenter.present()

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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_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">"Į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_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> <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_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_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_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_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_conversation_title">"Lämna konversation"</string>
<string name="screen_room_details_leave_room_title">"Lämna rum"</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.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.matrix.api.core.UserId 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.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -121,7 +122,25 @@ class RoomDetailsViewTest {
@Test @Test
fun `click on call invokes expected callback`() { 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( rule.setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
@ -129,7 +148,7 @@ class RoomDetailsViewTest {
), ),
onJoinCallClick = callback, 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(), openPollHistory: () -> Unit = EnsureNeverCalled(),
openMediaGallery: () -> Unit = EnsureNeverCalled(), openMediaGallery: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(), openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (CallIntent) -> Unit = EnsureNeverCalledWithParam(),
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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">"Į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_edition_error_title">"Nepavyko atnaujinti kambario"</string>
<string name="screen_room_details_updating_room">"Atnaujinamas kambarys…"</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_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">"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_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_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">"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> <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_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_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_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_lost_recovery_key">"Hukkasitko palautusavaimesi?"</string>
<string name="screen_recovery_key_confirm_success">"Palautusavain vahvistettu"</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_copied_to_clipboard">"Palautusavain kopioitu"</string>
<string name="screen_recovery_key_generating_key">"Luodaan…"</string> <string name="screen_recovery_key_generating_key">"Luodaan…"</string>
<string name="screen_recovery_key_save_action">"Tallenna palautusavain"</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_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_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_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_subtitle">"Vahvista, että haluat nollata identiteettisi."</string>
<string name="screen_reset_encryption_password_title">"Kirjoita tilisi salasana jatkaaksesi"</string> <string name="screen_reset_encryption_password_title">"Kirjoita tilisi salasana jatkaaksesi"</string>
</resources> </resources>

View file

@ -8,13 +8,16 @@
</plurals> </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">"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_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_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">"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_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_leave_space_title_last_owner">"Overfør eierskap"</string>
<string name="screen_space_add_room_action">"Rom"</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_empty_state_title">"Legg til ditt første rom"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</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"> <plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Fjern %1$d rom fra %2$s"</item> <item quantity="one">"Fjern %1$d rom fra %2$s"</item>
<item quantity="other">"Fjern %1$d rommene 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_leave_space_choose_owners_action">"Välj ägare"</string> <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_roles_and_permissions">"Roller och behörigheter"</string>
<string name="screen_space_settings_security_and_privacy">"Säkerhet och sekretess"</string> <string name="screen_space_settings_security_and_privacy">"Säkerhet och sekretess"</string>
</resources> </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_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_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_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_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_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> <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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -83,8 +84,14 @@ class UserProfileFlowNode(
callback.navigateToRoom(roomId) callback.navigateToRoom(roomId)
} }
override fun startCall(dmRoomId: RoomId) { override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) {
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId)) elementCallEntryPoint.startCall(
CallType.RoomCall(
sessionId = sessionId,
roomId = dmRoomId,
isAudioCall = callIntent == CallIntent.AUDIO
)
)
} }
override fun startVerifyUserFlow(userId: UserId) { override fun startVerifyUserFlow(userId: UserId) {

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