Merge branch 'develop' into feature-oled-black

This commit is contained in:
Timur Gilfanov 2026-03-30 11:08:53 +04:00 committed by GitHub
commit d0dcbab750
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1505 changed files with 14143 additions and 9545 deletions

View file

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

View file

@ -1,4 +1,12 @@
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
<!--
Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request.
Are you adding a new feature? Keep in mind that it needs to be added to [the iOS client](https://github.com/element-hq/element-x-ios) too, unless it's related to an Android OS only behaviour.
**IMPORTANT:** if you are adding new screens or modifying existing ones, this needs acceptance from the product and design teams before being merged. For this, it's better to start with a [feature request issue](https://github.com/element-hq/element-x-android/issues/new?template=enhancement.yml) describing the change you want to make and the motivation behind it instead of directly creating a pull request. This will allow the product and design teams to give feedback on the change before you start working on it, and avoid you doing work that might end up being rejected.
-->
## Content
@ -44,10 +52,13 @@ Uncomment this markdown table below and edit the last line `|||`:
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
- This PR was made with the help of AI:
- [ ] Yes. In this case, please request a review by Copilot.
- [ ] No.
- [ ] Changes have been tested on an Android device or Android emulator with API 24
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request title will be used in the release note, it clearly defines what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] You've made a self review of your PR

30
.github/renovate.json vendored
View file

@ -1,30 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"labels": [
"PR-Dependencies"
],
"ignoreDeps": [
"string:app_name",
"gradle"
],
"packageRules": [
{
"groupName": "kotlin",
"matchPackageNames": [
"/^org.jetbrains.kotlin/",
"/^com.google.devtools.ksp/",
"/^androidx.compose.compiler/"
]
},
{
"versioning": "semver",
"matchPackageNames": [
"/^org.maplibre/",
"/^org.jetbrains.kotlinx:kotlinx-datetime/"
]
}
]
}

47
.github/renovate.json5 vendored Normal file
View file

@ -0,0 +1,47 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
],
"labels": [
"PR-Dependencies",
],
"ignoreDeps": [
"string:app_name",
"gradle",
],
"minimumReleaseAge": "7 days",
"packageRules": [
{
"matchPackageNames": [
"org.matrix.rustcomponents:sdk-android",
"/^io.element.android/",
],
"minimumReleaseAge": null,
},
{
"groupName": "kotlin",
"matchPackageNames": [
"/^org.jetbrains.kotlin/",
"/^com.google.devtools.ksp/",
"/^androidx.compose.compiler/",
],
},
{
"versioning": "semver",
"matchPackageNames": [
"/^org.maplibre/",
"/^org.jetbrains.kotlinx:kotlinx-datetime/",
],
},
{
// Limit PostHog Android upgrade to one PR per month, the first day of the month
"matchPackageNames": [
"com.posthog:posthog-android",
],
"schedule": [
"* * 1 * *",
]
},
],
}

View file

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

View file

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

View file

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

View file

@ -2,20 +2,25 @@ name: Community PR notice
on:
workflow_dispatch:
pull_request_target:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
- reopened
permissions: {}
jobs:
welcome:
runs-on: ubuntu-latest
permissions:
# Require to comment the PR.
pull-requests: write
name: Welcome comment
# Only display it if base repo (upstream) is different from HEAD repo (possibly a fork)
if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name
steps:
- name: Add auto-generated commit warning
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.createComment({
@ -24,6 +29,7 @@ jobs:
repo: context.repo.repo,
body: `Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:
- If your pull request adds a feature or modifies the UI, this should have an equivalent pull request in the [Element X iOS repo](https://github.com/element-hq/element-x-ios) unless it only affects an Android-only behaviour or is behind a disabled feature flag, since we need parity in both clients to consider a feature done. It will also need to be approved by our product and design teams before being merged, so it's usually a good idea to discuss the changes in a Github issue first and then start working on them once the approach has been validated.
- Your branch should be based on \`origin/develop\`, at least when it was created.
- The title of the PR will be used for release notes, so it needs to describe the change visible to the user.
- The test pass locally running \`./gradlew test\`.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,11 +2,13 @@ name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
workflow_call:
workflow_call: # zizmor: ignore[dangerous-triggers]
secrets:
ELEMENT_BOT_TOKEN:
required: true
permissions: {}
jobs:
prevent-blocked:
name: Prevent blocked
@ -15,7 +17,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@ -39,7 +41,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.addLabels({
@ -52,13 +54,16 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
permissions:
# Require to comment and close the PR.
pull-requests: write
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.issues.createComment({

View file

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

View file

@ -5,6 +5,8 @@ on:
pull_request:
types: [ labeled ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
@ -12,6 +14,9 @@ env:
jobs:
record:
permissions:
# Need write permissions on PRs to remove the label "Record-Screenshots"
pull-requests: write
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'
@ -38,23 +43,23 @@ jobs:
labels: Record-Screenshots
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
with:
persist-credentials: false
- name: ☕️ Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,8 @@ on:
push:
branches: [ main, develop ]
permissions: {}
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
@ -47,13 +49,13 @@ jobs:
sudo swapon /mnt/swapfile
sudo swapon --show
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@ -61,21 +63,21 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports
if: failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: kover-error-report
path: |
@ -87,7 +89,7 @@ jobs:
- name: 🚫 Upload test results on error
if: failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: tests-and-screenshot-tests-results
path: |
@ -110,5 +112,5 @@ jobs:
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
files: app/build/reports/kover/reportGplayDebug.xml
files: build/reports/kover/reportMerged.xml
verbose: true

View file

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

View file

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

View file

@ -2,12 +2,14 @@ name: Validate Git LFS
on: [pull_request, merge_group]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4
- run: |
./tools/git/validate_lfs.sh

4
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.10" />
<option name="version" value="2.3.20" />
</component>
</project>
</project>

View file

@ -1,10 +1,10 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Enter recovery key"
visible: "Use recovery key"
timeout: 30000
- takeScreenshot: build/maestro/150-Verify
- tapOn: "Enter recovery key"
- tapOn: "Use recovery key"
- tapOn:
id: "verification-recovery_key"
- inputText: ${MAESTRO_RECOVERY_KEY}

118
AGENTS.md Normal file
View file

@ -0,0 +1,118 @@
# AGENTS.md — Element X Android
> **Repo:** `element-hq/element-x-android` — Android Matrix client (Compose UI + `matrix-rust-sdk`).
---
## Strong Conventions
PRs must meet these rules.
### Code Style
- Style enforced by **Editor config** (`.editorconfig`).
- Set "Hard wrap at" to 160 chars in Android Studio.
### PII & Logging
- We use **Timber** for logging. Never use `android.util.Log`.
- **Never log secrets, passwords, keys, or user content** (e.g. message bodies).
- Matrix IDs (User IDs, Room IDs, Event IDs) are safe to log.
### Strings & Localisation
- Default localisation: `en` (en-GB strings), shared with Element X iOS via [Localazy](https://localazy.com/p/element).
- **Never edit `localazy.xml`** — it is auto-generated and overwritten.
- New English strings go in **`temporary.xml`**. The core team imports these to Localazy.
- **Key naming**:
- Cross-screen verbs: `action_` (e.g., `action_copy`).
- Common nouns/other: `common_` (e.g., `common_error`).
- Accessibility: `a11y_`.
- Screen-specific: `screen_<name>_<key>` (e.g., `screen_onboarding_welcome_title`).
- Errors: `error_` prefix.
- Platform-specific: `_ios` or `_android` suffix.
- Placeholders: Use numbered form `%1$s`, `%2$d`.
### Previews
- Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency.
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
- Wrap previews in `ElementPreview { ... }`.
---
## Pull Request Guidelines
- Use sentence-style commit/PR messages (no conventional commits).
- Apply exactly **one** `PR-` label for changelog categorization.
- PR title = changelog entry — make it descriptive; no "Fixes #…" prefixes.
- Include screenshots or screen recordings for any UI changes.
- Keep PRs focused; split changes over 1000 lines.
---
## Project Structure
### Build System
Common Gradle tasks:
- Build: `./gradlew assembleDebug`
- Unit Tests: `./gradlew test`
- Lint: `./gradlew lint`
- Format: `./gradlew ktlintFormat`
- Update Docs TOC: `./gradlew generateDocsToc`
### Gradle Modules
Features follow a 3-module structure:
- `features/foo/api`: Public interfaces and data classes.
- `features/foo/impl`: Internal implementation, Presenter, and View.
- `features/foo/test`: Test fakes and utilities.
---
## Architecture: Appyx + Molecule
We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecule](https://github.com/cashapp/molecule) for Presenters.
### Files Per Screen (`Foo`)
| File | Purpose |
| :--- | :--- |
| `FooNode.kt` | Appyx Node: Handles navigation and wires the Presenter to the View. |
| `FooPresenter.kt` | A `@Composable` function that produces `FooState` from `FooEvent`s. |
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
---
## Dependency Injection (Metro)
- We use [Metro](https://zacsweers.github.io/metro/) for DI.
- Inject via constructor parameters using `@Inject`.
- Use `@AssistedInject` and `@AssistedFactory` for components requiring runtime arguments (like Navigators or IDs).
- Use `@ContributesBinding(AppScope::class)` for singleton-like services.
- Use `@ContributesNode(RoomScope::class)` for Appyx Nodes.
---
## Compound Design System
Always prefer Compound components and tokens from `libraries/compound/` module.
- **Colours**: `ElementTheme.colors.textPrimary`, `ElementTheme.colors.bgCanvasDefault`.
- **Typography**: `ElementTheme.typography.fontBodyMdRegular`.
- **Icons**: Use `CompoundIcons.IconName()` (e.g., `CompoundIcons.UserProfileSolid()`).
---
## The Rust SDK Layer
We wrap the `matrix-rust-sdk` to isolate the UI from the underlying SDK.
- Naming: SDK `Room``JoinedRoom` or `RoomInfo`.
- Type Mapping: Map Rust SDK types to Kotlin data classes in the `api` module to avoid leaking `MatrixRustSDK` into the UI.
- Always follow Kotlin naming conventions (e.g., `userId` instead of `userID`).

View file

@ -1,3 +1,157 @@
Changes in Element X v26.03.4
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.03.4 -->
## What's Changed
### ✨ Features
* Add a foreground service with a wakelock for fetching push notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6321
### 🙌 Improvements
* Iterate on send button colors by @bmarty in https://github.com/element-hq/element-x-android/pull/6314
### 🐛 Bugfixes
* Fix key storage if it's broken by @andybalaam in https://github.com/element-hq/element-x-android/pull/6290
* Improve error displayed when .well-known file is malformed by @bmarty in https://github.com/element-hq/element-x-android/pull/6370
* Fix crash when starting a DM by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6419
* Fix media seeking flicker by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6434
* Fix `TransactionTooLargeExceptions` caused by Appyx by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6410
* Fix wakelock not stopping early when notifications are disabled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6424
* Fix long messages not being clickable by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6356
* Fix: "Reset identity" flow leaves backup disabled #5075 by @andybalaam in https://github.com/element-hq/element-x-android/pull/6420
* Restore custom user certificate provider by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6451
### 🗣 Translations
* Sync Strings - iterate on wording about crypto identity by @ElementBot in https://github.com/element-hq/element-x-android/pull/6352
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6435
### 🧱 Build
* Limit number of created PR to upgrade Posthog dependency by @bmarty in https://github.com/element-hq/element-x-android/pull/6318
* Renovate: add a cooldown of 7 days for dependencies that we do not manage by @bmarty in https://github.com/element-hq/element-x-android/pull/6323
* Improve Kover setup by using only convention plugins by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6213
* Fix permissions issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/6355
* Fix permissions issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/6366
### 📄 Documentation
* Add warning about new features to pull request template by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6425
### Dependency upgrades
* fix(deps): update dependency com.posthog:posthog-android to v3.36.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6311
* fix(deps): update dependency com.posthog:posthog-android to v3.36.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6316
* chore(deps): update reactivecircus/android-emulator-runner action to v2.36.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6320
* fix(deps): update dependency com.posthog:posthog-android to v3.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6317
* chore(deps): update actions/download-artifact action to v8.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6324
* fix(deps): update dependency com.github.matrix-org:matrix-analytics-events to v0.33.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6313
* chore(deps): update plugin ktlint to v14.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6332
* fix(deps): update dependency androidx.compose:compose-bom to v2026.03.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6329
* fix(deps): update datastore to v1.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6326
* chore(deps): update webfactory/ssh-agent action to v0.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6325
* fix(deps): update activity to v1.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6327
* fix(deps): update dependency io.sentry:sentry-android to v8.35.0 and enable ANR profiling by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6331
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6411
* chore(deps): update reactivecircus/android-emulator-runner action to v2.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6430
* fix(deps): update media3 to v1.9.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6445
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6444
* fix(deps): update dependency androidx.compose.material3:material3 to v1.5.0-alpha15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6306
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.24 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6455
### Others
* fix(deps): update sqldelight to v2.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6343
* Remove matrix.to intent filter from the AndroidManifest. by @bmarty in https://github.com/element-hq/element-x-android/pull/6345
* Update wording of button "Enter recovery key" to "Use recovery key" by @bmarty in https://github.com/element-hq/element-x-android/pull/6357
* Fix room member not tappable in a Thread by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6416
* Fix keyboard not auto-opening when editing a message by @kalix127 in https://github.com/element-hq/element-x-android/pull/6412
* Design iteration on file attachment in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/6322
* fix(deps): update dependency org.maplibre.gl:android-sdk to v13.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6428
* Iterate on microphone icon by @bmarty in https://github.com/element-hq/element-x-android/pull/6452
* Increase icon size of audio and files in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/6453
* Fix voice recording being interrupted by notifications sounds by @kalix127 in https://github.com/element-hq/element-x-android/pull/6438
## New Contributors
* @bxdxnn made their first contribution in https://github.com/element-hq/element-x-android/pull/6416
* @kalix127 made their first contribution in https://github.com/element-hq/element-x-android/pull/6412
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.3...v26.03.4
Changes in Element X v26.03.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.03.3 -->
## What's Changed
### ✨ Features
* Support for Voice Call only (no video), parity with web by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5995
### 🐛 Bugfixes
* Fix read receipts not appearing in threaded timelines by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6297
* Try fixing index OOB issues in `Editable.checkSuggestionNeeded` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6303
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6302
### 🧱 Build
* Add zizmorcore/zizmor-action by @bmarty in https://github.com/element-hq/element-x-android/pull/6286
* Add use existing branch confirmation and progress for file download by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6294
* Replace `knit` with `generate_toc.py` script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6279
### Dependency upgrades
* Update plugin sonarqube to v7.2.3.7755 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6283
* Update dependency io.sentry:sentry-android to v8.34.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6289
* Update dependency org.matrix.rustcomponents:sdk-android to v26.03.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6292
* Update dependency com.posthog:posthog-android to v3.35.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6293
* Update zizmorcore/zizmor-action action to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6299
* fix(deps): update dependency org.maplibre.gl:android-sdk to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6277
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.09 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6307
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6310
### Others
* Add code to help debugging the saved nav state graph by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6295
* Add network constraints for fetching notifications with `WorkManager` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6305
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.2...v26.03.3
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
=============================

1
CLAUDE.md Normal file
View file

@ -0,0 +1 @@
@AGENTS.md

View file

@ -10,13 +10,14 @@
* [I want to add new strings to the project](#i-want-to-add-new-strings-to-the-project)
* [I want to help translating Element](#i-want-to-help-translating-element)
* [Element X Android Gallery](#element-x-android-gallery)
* [I want to add a new feature to Element X Android](#i-want-to-add-a-new-feature-to-element-x-android)
* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue)
* [Kotlin](#kotlin)
* [Changelog](#changelog)
* [Code quality](#code-quality)
* [detekt](#detekt)
* [ktlint](#ktlint)
* [knit](#knit)
* [checkDocs](#checkdocs)
* [lint](#lint)
* [Unit tests](#unit-tests)
* [konsist](#konsist)
@ -76,6 +77,14 @@ Once added to Localazy, translations can be checked screen per screen using our
Localazy syncs occur every Monday and the screenshots on this page are generated every Tuesday, so you'll have to wait to see your change appearing on Element X Android Gallery.
## I want to add a new feature to Element X Android
Thank you for contributing to the project! Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request.
Also, please keep in mind that any feature added to Element X Android needs to be added to [the iOS client](https://github.com/element-hq/element-x-ios) too, unless it's related to an Android OS only behaviour.
**IMPORTANT:** if you are adding new screens or modifying existing ones, this needs acceptance from the product and design teams before being merged. For this, it's better to start with a [feature request issue](https://github.com/element-hq/element-x-android/issues/new?template=enhancement.yml) describing the change you want to make and the motivation behind it instead of directly creating a pull request. This will allow the product and design teams to give feedback on the change before you start working on it, and avoid you doing work that might end up being rejected.
## I want to submit a PR to fix an issue
Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request.
@ -123,13 +132,13 @@ Note that you can run
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
#### knit
#### checkDocs
[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files.
`checkDocs` is a Gradle task which checks markdown files on the project to ensure their table of contents is up to date. It uses `tools/docs/generate_toc.py --verify` under the hood, and has a counterpart `generateDocsToc` task which runs `tools/docs/generate_toc.py` to update the table of contents of markdown files.
So everytime the toc should be updated, just run
<pre>
./gradlew knit
./gradlew generateDocsToc
</pre>
and commit the changes.
@ -137,7 +146,7 @@ and commit the changes.
The CI will check that markdown files are up to date by running
<pre>
./gradlew knitCheck
./gradlew checkDocs
</pre>
#### lint
@ -184,7 +193,7 @@ internal fun PinIconPreview() = ElementPreview {
}
```
This will allow to preview the composable in both light and dark mode in Android Studio. This will also automatically add UI tests. The GitHub action [Record screenshots](https://github.com/element-hq/element-x-android/actions/workflows/recordScreenshots.yml) has to be run to record the new screenshots. The PR reviewer can trigger this for you if you're not part of the core team.
This will allow to preview the composable in both light and dark mode in Android Studio. This will also automatically add UI tests. The GitHub action [Record screenshots](https://github.com/element-hq/element-x-android/actions/workflows/recordScreenshots.yml) has to be run to record the new screenshots. The PR reviewer can trigger this for you if you're not part of the core team.
### Authors

View file

@ -6,6 +6,5 @@
* Please see LICENSE files in the repository root for full details.
*/
plugins {
alias(libs.plugins.kotlin.jvm)
id("com.android.lint")
id("io.element.jvm-library")
}

View file

@ -21,10 +21,8 @@ import extension.allFeaturesImpl
import extension.allLibrariesImpl
import extension.allServicesImpl
import extension.buildConfigFieldStr
import extension.koverDependencies
import extension.locales
import extension.setupDependencyInjection
import extension.setupKover
import extension.testCommonDependencies
import java.util.Locale
@ -33,7 +31,6 @@ plugins {
alias(libs.plugins.kotlin.android)
// When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit)
id("kotlin-parcelize")
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
@ -41,8 +38,6 @@ plugins {
// alias(libs.plugins.gms.google.services)
}
setupKover()
android {
namespace = "io.element.android.x"
@ -250,26 +245,6 @@ androidComponents {
configureLicensesTasks(reportingExtension)
}
// Knit
apply {
plugin("kotlinx-knit")
}
knit {
files = fileTree(project.rootDir) {
include(
"**/*.md",
"**/*.kt",
"*/*.kts",
)
exclude(
"**/build/**",
"*/.gradle/**",
"**/CHANGES.md",
)
}
}
setupDependencyInjection()
dependencies {
@ -316,8 +291,6 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)
koverDependencies()
}
tasks.withType<GenerateBuildConfig>().configureEach {
@ -334,6 +307,7 @@ licensee {
allow("BSD-2-Clause")
allow("BSD-3-Clause")
allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")

View file

@ -74,3 +74,6 @@
# Keep Metro classes
-keep,allowoptimization,allowshrinking class dev.zacsweers.metro.** { *; }
# Rustls Platform Verifier
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }

View file

@ -118,20 +118,6 @@
<data android:host="mobile.element.io" />
<data android:path="/element/" />
</intent-filter>
<!--
matrix.to links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
https://developer.android.com/training/app-links#web-links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
<!--
matrix: links
-->

View file

@ -9,6 +9,8 @@
package io.element.android.x
import android.app.Application
import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.startup.AppInitializer
import androidx.work.Configuration
import dev.zacsweers.metro.createGraphFactory
@ -27,6 +29,7 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build()
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() {
super.onCreate()
AppInitializer.getInstance(this).apply {
@ -36,5 +39,9 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
}
logApplicationInfo(this)
// Disable the strict offset check for anchored draggable components, as it can cause issues with bottom sheets.
// Remove once https://issuetracker.google.com/issues/477038695 is fixed.
isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false
}
}

View file

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

View file

@ -57,6 +57,7 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.preferences.test)

View file

@ -8,7 +8,6 @@
package io.element.android.appnav
import android.content.Intent
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@ -63,6 +62,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@ -307,7 +307,7 @@ class LoggedInFlowNode(
data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val intent: Intent) : NavTarget
data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
@ -570,7 +570,7 @@ class LoggedInFlowNode(
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(intent = navTarget.intent),
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) {
// Remove the incoming share screen
@ -649,13 +649,13 @@ class LoggedInFlowNode(
}
}
internal suspend fun attachIncomingShare(intent: Intent) {
internal suspend fun attachIncomingShare(shareIntentData: ShareIntentData) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
NavTarget.IncomingShare(intent)
NavTarget.IncomingShare(shareIntentData)
)
}
}

View file

@ -44,6 +44,7 @@ import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.BackstackView
@ -265,7 +266,7 @@ class RootFlowNode(
@Parcelize data class AccountSelect(
val currentSessionId: SessionId,
val intent: Intent?,
val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?,
) : NavTarget
@ -357,8 +358,8 @@ class RootFlowNode(
backstack.pop()
}
attachSession(sessionId).apply {
if (navTarget.intent != null) {
attachIncomingShare(navTarget.intent)
if (navTarget.shareIntentData != null) {
attachIncomingShare(navTarget.shareIntentData)
} else if (navTarget.permalinkData != null) {
attachPermalinkData(navTarget.permalinkData)
}
@ -392,7 +393,7 @@ class RootFlowNode(
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.intent)
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData)
}
}
@ -423,7 +424,7 @@ class RootFlowNode(
}
}
private suspend fun onIncomingShare(intent: Intent) {
private suspend fun onIncomingShare(shareIntentData: ShareIntentData) {
// Is there a session already?
val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) {
@ -437,13 +438,13 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
intent = intent,
shareIntentData = shareIntentData,
permalinkData = null,
)
)
} else {
// Only one account, directly attach the incoming share node.
loggedInFlowNode.attachIncomingShare(intent)
loggedInFlowNode.attachIncomingShare(shareIntentData)
}
}
}
@ -467,7 +468,7 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
intent = null,
shareIntentData = null,
permalinkData = permalinkData,
)
)

View file

@ -12,6 +12,8 @@ import android.content.Intent
import dev.zacsweers.metro.Inject
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.ShareIntentHandler
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
@ -25,7 +27,7 @@ sealed interface ResolvedIntent {
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
data class Login(val params: LoginParams) : ResolvedIntent
data class IncomingShare(val intent: Intent) : ResolvedIntent
data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
}
@Inject
@ -34,6 +36,7 @@ class IntentResolver(
private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
private val shareIntentHandler: ShareIntentHandler,
) {
fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null
@ -62,7 +65,8 @@ class IntentResolver(
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
if (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) {
return ResolvedIntent.IncomingShare(intent)
val data = shareIntentHandler.handleIncomingShareIntent(intent) ?: return null
return ResolvedIntent.IncomingShare(data)
}
// Unknown intent

View file

@ -167,6 +167,6 @@ class LoggedInPresenter(
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
matrixClient.getAccountManagementUrl(AccountManagementAction.Profile)
matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList)
matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList)
}
}

View file

@ -15,6 +15,9 @@ import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.UriToShare
import io.element.android.features.share.test.FakeShareIntentHandler
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
@ -239,26 +242,34 @@ class IntentResolverTest {
@Test
fun `test incoming share simple`() {
val shareIntentData = ShareIntentData.PlainText("Hello")
val sut = createIntentResolver(
oidcIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hello")
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(intent = intent))
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(shareIntentData))
}
@Test
fun `test incoming share multiple`() {
val fileUri = "content://com.example.app/file1.jpg".toUri()
val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg")))
val sut = createIntentResolver(
oidcIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND_MULTIPLE
putExtra(Intent.EXTRA_TEXT, "Hello")
data = fileUri
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(intent = intent))
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(shareIntentData))
}
@Test
@ -296,6 +307,7 @@ class IntentResolverTest {
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
): IntentResolver {
return IntentResolver(
deeplinkParser = { deeplinkParserResult },
@ -308,6 +320,9 @@ class IntentResolverTest {
permalinkParser = FakePermalinkParser(
result = permalinkParserResult
),
shareIntentHandler = FakeShareIntentHandler(
onIncomingShareIntent = onIncomingShareIntent,
),
)
}
}

View file

@ -81,7 +81,7 @@ class LoggedInPresenterTest {
accountManagementUrlResult.assertions().isCalledExactly(2)
.withSequence(
listOf(value(AccountManagementAction.Profile)),
listOf(value(AccountManagementAction.SessionsList)),
listOf(value(AccountManagementAction.DevicesList)),
)
}
}

View file

@ -76,6 +76,9 @@ allprojects {
filter {
exclude { element -> element.file.path.contains(generatedPath) }
exclude("io/element/android/tests/konsist/failures/**")
// This file comes from another project and we want to keep it as close to the original as possible
exclude("**/SafeChildrenTransitionScope.kt")
}
}
// Dependency check
@ -175,12 +178,23 @@ tasks.register("runQualityChecks") {
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
}
dependsOn(":app:knitCheck")
dependsOn("checkDocs")
// Make sure all checks run even if some fail
gradle.startParameter.isContinueOnFailure = true
}
// Register Markdown documentation check task.
tasks.register("checkDocs", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", "--verify", *inputs.files.map { it.path }.toTypedArray())
}
// Register Markdown documentation TOC generation task.
tasks.register("generateDocsToc", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", *inputs.files.map { it.path }.toTypedArray())
}
// Make sure to delete old screenshots before recording new ones
subprojects {
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")

View file

@ -11,6 +11,7 @@
* [Rust SDK](#rust-sdk)
* [Matrix Rust Component Kotlin](#matrix-rust-component-kotlin)
* [Building the SDK locally](#building-the-sdk-locally)
* [rustls and platform verifier](#rustls-and-platform-verifier)
* [The Android project](#the-android-project)
* [Application](#application)
* [Jetpack Compose](#jetpack-compose)
@ -144,8 +145,8 @@ Prerequisites:
```
You can then build the Rust SDK by running the script
[`tools/sdk/build_rust_sdk.sh`](../tools/sdk/build_rust_sdk.sh) and just answering
the questions.
[`tools/sdk/build-rust-sdk`](../tools/sdk/build-rust-sdk). Type
`./tools/sdk/build-rust-sdk --help` for help.
This will prompt you for the path to the Rust SDK, then build it and
`matrix-rust-components-kotlin`, eventually producing an aar file at
@ -160,6 +161,19 @@ Troubleshooting:
You can switch back to using the published version of the SDK by deleting `libraries/rustsdk/matrix-rust-sdk.aar`.
#### rustls and platform verifier
The SDK uses [rustls](https://github.com/rustls/rustls) for TLS, which is a pure Rust implementation of TLS. In turn, this means we have to add the
`rustls-platform-verifier` library to our project, which provides platform-specific TLS certificate verification for rustls. This library uses the Android NDK's
`TrustManager` to verify TLS certificates on Android.
Though it's meant to be used through convoluted way of downloading the dependency, locating it in the
cargo folder and using that path as a local maven repo as described [here](https://github.com/rustls/rustls-platform-verifier#android), we have
added a script (`tools/sdk/update-rustls`) to download, unpack and add this AAR file locally to the `:libraries:matrix:impl` module instead.
When should we run this script? Whenever we update the `rustls` dependency in the Rust SDK, we should check if the version of `rustls-platform-verifier`
has changed as well, and if so, run this script to update the AAR file in our project. The SDK team should ping us when this happens.
### The Android project
The project should compile out of the box.

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -0,0 +1,2 @@
Main changes in this version: fixed an issue that could cause a crash when instantiating the cryptographic database.
Full changelog: https://github.com/element-hq/element-x-android/releases

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

@ -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

@ -3,5 +3,5 @@
<string name="screen_analytics_settings_help_us_improve">"Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."</string>
<string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"здесь"</string>
<string name="screen_analytics_settings_share_data">"Отправлять аналитические данные"</string>
<string name="screen_analytics_settings_share_data">"Отправлять аналитику"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
<string name="screen_analytics_settings_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"Bu yerga"</string>
<string name="screen_analytics_settings_read_terms">"Barcha shartlar bilan %1$s tanishib chiqishingiz mumkin."</string>
<string name="screen_analytics_settings_read_terms_content_link">"bu yerda"</string>
<string name="screen_analytics_settings_share_data">"Analitik ma\'lumotlarni ulashish"</string>
</resources>

View file

@ -2,9 +2,9 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Biz hech qanday shaxsiy ma\'lumotlarni yozmaymiz yoki profilga kiritmaymiz"</string>
<string name="screen_analytics_prompt_help_us_improve">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
<string name="screen_analytics_prompt_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"Bu yerga"</string>
<string name="screen_analytics_prompt_read_terms">"Barcha shartlar bilan %1$s tanishib chiqishingiz mumkin."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"bu yerda"</string>
<string name="screen_analytics_prompt_settings">"Buni istalgan vaqtda oʻchirib qoʻyishingiz mumkin"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Biz sizning ma\'lumotlaringizni uchinchi tomonlar bilan baham ko\'rmaymiz"</string>
<string name="screen_analytics_prompt_title">"Yaxshilashga yordam bering%1$s"</string>
<string name="screen_analytics_prompt_title">"%1$sʼni yaxshilashga yordam bering"</string>
</resources>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Se grupper, du har oprettet eller tilmeldt dig"</string>
<string name="screen_space_announcement_item2">"Acceptere eller afvise invitationer til grupper"</string>
<string name="screen_space_announcement_item3">"Finde alle rum, du kan deltage i, i dine grupper"</string>
<string name="screen_space_announcement_item4">"Deltage i offentlige grupper"</string>
<string name="screen_space_announcement_item5">"Forlade de grupper, du har tilsluttet dig"</string>
<string name="screen_space_announcement_notice">"Filtrering, oprettelse og administration af grupper kommer snart."</string>
<string name="screen_space_announcement_subtitle">"Velkommen til betaversionen af Grupper! Med denne første version kan du:"</string>
<string name="screen_space_announcement_title">"Introduktion til Grupper"</string>
<string name="screen_space_announcement_item1">"Se klynger, du har oprettet eller tilmeldt dig"</string>
<string name="screen_space_announcement_item2">"Acceptere eller afvise invitationer til klynger"</string>
<string name="screen_space_announcement_item3">"Finde alle rum, du kan deltage i, i dine klynger"</string>
<string name="screen_space_announcement_item4">"Deltage i offentlige klynger"</string>
<string name="screen_space_announcement_item5">"Forlade de klynger, du har tilsluttet dig"</string>
<string name="screen_space_announcement_notice">"Filtrering, oprettelse og administration af klynger kommer snart."</string>
<string name="screen_space_announcement_subtitle">"Velkommen til betaversionen af Klynger! Med denne første version kan du:"</string>
<string name="screen_space_announcement_title">"Introduktion til Klynger"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"직접 만들거나 참여 중인 스페이스 보기"</string>
<string name="screen_space_announcement_item2">"스페이스 초대 수락 또는 거절"</string>
<string name="screen_space_announcement_item3">"참여 가능한 스페이스 내 모든 방 탐색"</string>
<string name="screen_space_announcement_item4">"공개 스페이스 참여"</string>
<string name="screen_space_announcement_item5">"참여 중인 스페이스 나가기"</string>
<string name="screen_space_announcement_notice">"스페이스 필터링, 생성 및 관리 기능이 곧 추가될 예정입니다."</string>
<string name="screen_space_announcement_subtitle">"스페이스 베타 버전에 오신 것을 환영합니다! 이번 첫 번째 버전에서는 다음과 같은 기능을 이용하실 수 있습니다.:"</string>
<string name="screen_space_announcement_title">"스페이스 소개"</string>
</resources>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item1">"Просмотреть пространства, которые вы создали или к которым присоединились"</string>
<string name="screen_space_announcement_item1">"Просматривать пространства, которые вы создали или к которым присоединились"</string>
<string name="screen_space_announcement_item2">"Принимать или отклонять приглашения в пространства"</string>
<string name="screen_space_announcement_item3">"Найти все комнаты, к которым можно присоединиться в ваших пространствах"</string>
<string name="screen_space_announcement_item3">"Находить все комнаты, к которым можно присоединиться в ваших пространствах"</string>
<string name="screen_space_announcement_item4">"Присоединяться к публичным пространствам"</string>
<string name="screen_space_announcement_item5">"Покинуть все пространства, к которым вы присоединились"</string>
<string name="screen_space_announcement_notice">"Работа с пространствами скоро станет доступна"</string>
<string name="screen_space_announcement_subtitle">"Добро пожаловать в бета-версию пространств! В этой первой версии вы сможете:"</string>
<string name="screen_space_announcement_item5">"Покидать все пространства, к которым вы присоединились"</string>
<string name="screen_space_announcement_notice">"Фильтровать, создавать пространства и управлять ими можно будет позже."</string>
<string name="screen_space_announcement_subtitle">"Добро пожаловать в бета-версию пространств! Сейчас вы сможете:"</string>
<string name="screen_space_announcement_title">"Представляем пространства"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_announcement_item3">"Знаходьте у своїх просторах кімнати, до яких можна приєднатися"</string>
<string name="screen_space_announcement_notice">"Фільтрування, створення та керування просторами стане доступним найближчим часом."</string>
<string name="screen_space_announcement_subtitle">"Ласкаво просимо до бета-версії Просторів! У цій першій версії ви можете:"</string>
<string name="screen_space_announcement_title">"Представляємо Простори"</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> {
override val values: Sequence<CallNotificationData>
get() = sequenceOf(
aCallNotificationData(
audioOnly = false
),
aCallNotificationData(
audioOnly = true
),
)
}
internal fun aCallNotificationData(
audioOnly: Boolean
): CallNotificationData {
return CallNotificationData(
sessionId = SessionId("@alice:matrix.org"),
roomId = RoomId("!1234:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"),
senderId = UserId("@bob:matrix.org"),
roomName = "A room",
senderName = "Bob",
avatarUrl = null,
notificationChannelId = "incoming_call",
timestamp = 0L,
textContent = null,
expirationTimestamp = 1000L,
audioOnly = audioOnly
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Текущ разговор"</string>
<string name="call_foreground_service_message_android">"Докоснете, за да се върнете към разговора"</string>
<string name="call_foreground_service_title_android">"☎️ Разговор в ход"</string>
</resources>

View file

@ -3,6 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Текущий вызов"</string>
<string name="call_foreground_service_message_android">"Коснитесь, чтобы вернуться к вызову"</string>
<string name="call_foreground_service_title_android">"☎️ Идёт вызов"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Функция Element Call не поддерживает использование аудиоустройств Bluetooth в данной версии Android. Пожалуйста, выберите другое аудиоустройство."</string>
<string name="screen_incoming_call_subtitle_android">"Входящий вызов Element"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call не поддерживает использование устройств Bluetooth в данной версии Android. Пожалуйста, выберите другое устройство."</string>
<string name="screen_incoming_call_subtitle_android">"Входящий вызов Element Call"</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,10 @@
<string name="screen_create_room_action_create_room">"Nyt rum"</string>
<string name="screen_create_room_add_people_title">"Invitér andre"</string>
<string name="screen_create_room_error_creating_room">"Der opstod en fejl ved oprettelsen af rummet"</string>
<string name="screen_create_room_error_creating_space">"Gruppen kunne ikke oprettes på grund af en ukendt fejl. Prøv igen senere."</string>
<string name="screen_create_room_error_creating_space">"Klyngen kunne ikke oprettes på grund af en ukendt fejl. Prøv igen senere."</string>
<string name="screen_create_room_name_placeholder">"Tilføj navn…"</string>
<string name="screen_create_room_new_room_title">"Nyt rum"</string>
<string name="screen_create_room_new_space_title">"Ny gruppe"</string>
<string name="screen_create_room_new_space_title">"Ny klynge"</string>
<string name="screen_create_room_private_option_description">"Kun inviterede personer kan deltage."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Alle kan finde dette rum.
@ -27,7 +27,10 @@ Du kan ændre dette når som helst i rummets indstillinger."</string>
<string name="screen_create_room_room_address_section_footer">"Hvis dette rum skal være synligt i det offentlige register, skal du bruge en adresse."</string>
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Rummets synlighed"</string>
<string name="screen_create_room_space_selection_no_space_description">"(ingen klynge)"</string>
<string name="screen_create_room_space_selection_no_space_option">"Tilføj ikke til et mellemrum"</string>
<string name="screen_create_room_space_selection_no_space_title">"Ingen klynge valgt"</string>
<string name="screen_create_room_space_selection_sheet_title">"Tilføj til klynge"</string>
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
<string name="screen_create_room_topic_placeholder">"Tilføj beskrivelse…"</string>
</resources>

View file

@ -3,6 +3,7 @@
<string name="screen_create_room_action_create_room">"Nova soba"</string>
<string name="screen_create_room_add_people_title">"Pozovi osobe"</string>
<string name="screen_create_room_error_creating_room">"Došlo je do pogreške prilikom stvaranja sobe"</string>
<string name="screen_create_room_new_room_title">"Nova soba"</string>
<string name="screen_create_room_private_option_description">"Samo pozvane osobe mogu pristupiti ovoj sobi. Sve su poruke sveobuhvatno šifrirane."</string>
<string name="screen_create_room_public_option_description">"Svatko može pronaći ovu sobu.
To možete u svakom trenutku promijeniti u postavkama sobe."</string>

View file

@ -6,7 +6,7 @@
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ijin administrator atau moderator"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>

View file

@ -3,13 +3,34 @@
<string name="screen_create_room_action_create_room">"새 방"</string>
<string name="screen_create_room_add_people_title">"사람 초대하기"</string>
<string name="screen_create_room_error_creating_room">"방을 생성하던 중 오류가 발생했어요"</string>
<string name="screen_create_room_private_option_description">"초대받은 사람만 이 방에 액세스할 수 있습니다. 모든 메시지는 종단 간 암호화됩니다."</string>
<string name="screen_create_room_error_creating_space">"알 수 없는 오류로 인해 스페이스를 생성할 수 없습니다. 나중에 다시 시도해 주세요."</string>
<string name="screen_create_room_name_placeholder">"이름 추가…"</string>
<string name="screen_create_room_new_room_title">"새 방"</string>
<string name="screen_create_room_new_space_title">"새 스페이스"</string>
<string name="screen_create_room_private_option_description">"초대된 사람만 참여할 수 있습니다."</string>
<string name="screen_create_room_private_option_title">"비공개"</string>
<string name="screen_create_room_public_option_description">"누구나 이 방을 찾을 수 있습니다.
방 설정에서 언제든지 변경할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"누구나 방에 참여 요청을 할 수 있지만, 관리자나 운영자가 요청을 수락해야 합니다."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"참가 요청"</string>
<string name="screen_create_room_room_access_section_public_option_description">"누구나 이 방에 참여할 수 있습니다."</string>
<string name="screen_create_room_room_address_section_footer">"이 방이 공개 방 디렉토리에 표시되려면 방 주소가 필요합니다."</string>
<string name="screen_create_room_public_option_short_description">"누구나 참여할 수 있습니다."</string>
<string name="screen_create_room_public_option_title">"공개"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"누구나 참여를 요청할 수 있지만, 관리자나 중재자가 요청을 승인해야 합니다."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"참여 요청 허용"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"%1$s의 멤버는 누구나 참여할 수 있지만, 그 외의 사람은 액세스 요청을 해야 합니다."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"참여 요청하기"</string>
<string name="screen_create_room_room_access_section_private_option_description">"초대받은 사람만 참여할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_private_option_title">"비공개"</string>
<string name="screen_create_room_room_access_section_public_option_description">"누구나 참여할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_public_option_title">"공개"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s의 멤버는 누구나 참여할 수 있습니다."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"표준"</string>
<string name="screen_create_room_room_access_section_title">"액세스 권한이 있는 사용자"</string>
<string name="screen_create_room_room_address_section_footer">"공개 디렉터리에 표시하려면 주소가 필요합니다."</string>
<string name="screen_create_room_room_address_section_title">"주소"</string>
<string name="screen_create_room_room_visibility_section_title">"방 표시 여부"</string>
<string name="screen_create_room_space_selection_no_space_description">"(스페이스 없음)"</string>
<string name="screen_create_room_space_selection_no_space_option">"스페이스에 추가하지 않음"</string>
<string name="screen_create_room_space_selection_no_space_title">"홈"</string>
<string name="screen_create_room_space_selection_sheet_title">"스페이스에 추가"</string>
<string name="screen_create_room_topic_label">"주제 (선택)"</string>
<string name="screen_create_room_topic_placeholder">"설명 추가…"</string>
</resources>

View file

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

View file

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

View file

@ -4,27 +4,32 @@
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
<string name="screen_create_room_error_creating_space">"Не удалось создать это пространство из-за неизвестной ошибки. Попробуйте позже."</string>
<string name="screen_create_room_name_placeholder">"Добавьте имя…"</string>
<string name="screen_create_room_name_placeholder">"Добавить имя…"</string>
<string name="screen_create_room_new_room_title">"Новая комната"</string>
<string name="screen_create_room_new_space_title">"Новое пространство"</string>
<string name="screen_create_room_private_option_description">"Присоединиться могут только приглашенные."</string>
<string name="screen_create_room_private_option_title">"Приватный"</string>
<string name="screen_create_room_public_option_description">"Любой желающий может найти эту комнату.
Вы можете изменить это в любое время в настройках комнаты."</string>
<string name="screen_create_room_public_option_short_description">"Присоединиться может любой."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Разрешить запрос на присоединение"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Любой из %1$s может присоединиться, а всем остальным нужно запросить доступ."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Подать заявку на вступление"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Присоединиться могут только приглашенные."</string>
<string name="screen_create_room_room_access_section_public_option_description">"Присоединиться может любой желающий."</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Любой в %1$s может присоединиться."</string>
<string name="screen_create_room_public_option_short_description">"Кто угодно может присоединиться."</string>
<string name="screen_create_room_public_option_title">"Публичный"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Кто угодно может подать заявку, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Разрешить запросы на присоединение"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Кто угодно в %1$s может присоединиться, а всем остальным нужно запросить доступ."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Запросы на присоединение"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Присоединиться можно только по приглашениям."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Приватный"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Кто угодно может присоедниться."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Публичный"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Кто угодно в %1$s может присоединиться."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Стандарт"</string>
<string name="screen_create_room_room_access_section_title">"Кто имеет доступ"</string>
<string name="screen_create_room_room_address_section_footer">"Вам понадобится адрес комнаты, чтобы сделать ее видимой в каталоге."</string>
<string name="screen_create_room_room_address_section_footer">"Необходимо задать адрес комнаты, чтобы опубликовать ее в каталог комнат."</string>
<string name="screen_create_room_room_address_section_title">"Адрес"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимость комнаты"</string>
<string name="screen_create_room_space_selection_no_space_description">"(нет пространства)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Главная"</string>
<string name="screen_create_room_space_selection_no_space_option">"Не добавлять в пространство"</string>
<string name="screen_create_room_space_selection_no_space_title">"Пространство не выбрано"</string>
<string name="screen_create_room_space_selection_sheet_title">"Добавить в пространство"</string>
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
<string name="screen_create_room_topic_placeholder">"Добавить описание…"</string>

View file

@ -3,14 +3,24 @@
<string name="screen_create_room_action_create_room">"Нова кімната"</string>
<string name="screen_create_room_add_people_title">"Запросити людей"</string>
<string name="screen_create_room_error_creating_room">"Під час створення кімнати сталася помилка"</string>
<string name="screen_create_room_private_option_description">"Лише запрошені люди мають доступ до цієї кімнати. Усі повідомлення захищені наскрізним шифруванням."</string>
<string name="screen_create_room_name_placeholder">"Додати назву…"</string>
<string name="screen_create_room_new_room_title">"Нова кімната"</string>
<string name="screen_create_room_new_space_title">"Новий простір"</string>
<string name="screen_create_room_private_option_description">"Можуть приєднатися лише запрошені люди."</string>
<string name="screen_create_room_public_option_description">"Будь-хто може знайти цю кімнату.
Ви можете змінити це в будь-який час у налаштуваннях кімнати."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Будь-хто може попросити приєднатися до кімнати, але адміністратор або модератор повинен буде прийняти запит"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Запросити приєднатися"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Будь-хто може приєднатися до цієї кімнати"</string>
<string name="screen_create_room_room_address_section_footer">"Щоб цю кімнату було видно в каталозі загальнодоступних кімнат, вам знадобиться її адреса."</string>
<string name="screen_create_room_room_address_section_title">"Адреса кімнати"</string>
<string name="screen_create_room_public_option_short_description">"Приєднатися може будь-хто."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Будь-хто може подати запит на приєднання, але адміністратор або модератор повинен схвалити запит."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Дозволити запит на приєднання"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Приєднатися можуть лише запрошені особи."</string>
<string name="screen_create_room_room_access_section_public_option_description">"Приєднатися може будь-хто."</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Приєднатися може будь-хто з %1$s."</string>
<string name="screen_create_room_room_access_section_title">"Хто має доступ"</string>
<string name="screen_create_room_room_address_section_footer">"Вам знадобиться адреса, щоб зробити її видимою в загальнодоступному каталозі."</string>
<string name="screen_create_room_room_address_section_title">"Адреса"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимість кімнати"</string>
<string name="screen_create_room_space_selection_no_space_title">"Головна"</string>
<string name="screen_create_room_space_selection_sheet_title">"Додати до простору"</string>
<string name="screen_create_room_topic_label">"Тема (необов\'язково)"</string>
<string name="screen_create_room_topic_placeholder">"Додати опис…"</string>
</resources>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Вы уверены, что хотите отключить свою учётную запись? Данное действие не может быть отменено."</string>
<string name="screen_deactivate_account_confirmation_dialog_content">"Вы уверены, что хотите отключить свою учётную запись? Данное действие необратимо."</string>
<string name="screen_deactivate_account_delete_all_messages">"Удалить все мои сообщения"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Предупреждение: будущие пользователи могут увидеть незавершенные разговоры."</string>
<string name="screen_deactivate_account_description">"Отключение вашей учетной записи %1$s и означает следующее:"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Внимание: в будущем пользователи могут видеть неполные переписки."</string>
<string name="screen_deactivate_account_description">"Деактивация вашего аккаунта %1$s и означает следующее:"</string>
<string name="screen_deactivate_account_description_bold_part">"необратимо"</string>
<string name="screen_deactivate_account_list_item_1">"Ваша учётная запись будет %1$s (вы не сможете войти в неё снова, и ваш ID не может быть использован повторно)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"отключена навсегда"</string>
<string name="screen_deactivate_account_list_item_1">"Ваша учётная запись будет %1$s (вы не сможете войти в неё снова, и другие пользователи не смогут использовать ваше имя пользователя)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Отключить навсегда"</string>
<string name="screen_deactivate_account_list_item_2">"Вы будете удалены из всех чатов."</string>
<string name="screen_deactivate_account_list_item_3">"Данные вашей учётной записи будут удалены с нашего сервера идентификации."</string>
<string name="screen_deactivate_account_list_item_3">"Данные Вашего аккаунта будут удалены с нашего сервера идентификации."</string>
<string name="screen_deactivate_account_list_item_4">"Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."</string>
<string name="screen_deactivate_account_title">"Отключить учётную запись"</string>
</resources>

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
@ -30,28 +31,32 @@ class ChooseSelfVerificationModePresenter(
@Composable
override fun present(): ChooseSelfVerificationModeState {
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
.mapState { recoveryState ->
when (recoveryState) {
RecoveryState.WAITING_FOR_SYNC,
RecoveryState.UNKNOWN -> AsyncData.Loading()
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
RecoveryState.ENABLED,
RecoveryState.DISABLED -> AsyncData.Success(false)
val canUseRecoveryKey by produceState<AsyncData<Boolean>>(AsyncData.Uninitialized) {
encryptionService.recoveryStateStateFlow
.mapState { recoveryState ->
when (recoveryState) {
RecoveryState.WAITING_FOR_SYNC,
RecoveryState.UNKNOWN -> AsyncData.Loading()
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
RecoveryState.ENABLED,
RecoveryState.DISABLED -> AsyncData.Success(false)
}
}
}
.collectAsState()
.collect {
value = it
}
}
val buttonsState by remember {
derivedStateOf {
val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
val canUseRecoveryKey = canUseRecoveryKey.dataOrNull()
if (canUseAnotherDevice == null || canUseRecoveryKey == null) {
AsyncData.Loading()
} else {
AsyncData.Success(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
canUseRecoveryKey = canUseRecoveryKey,
)
)
}

View file

@ -18,6 +18,6 @@ data class ChooseSelfVerificationModeState(
) {
data class ButtonsState(
val canUseAnotherDevice: Boolean,
val canEnterRecoveryKey: Boolean,
val canUseRecoveryKey: Boolean,
)
}

View file

@ -17,22 +17,22 @@ class ChooseSelfVerificationModeStateProvider :
override val values = sequenceOf(
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
aButtonsState(canUseAnotherDevice = false, canUseRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
aButtonsState(canUseAnotherDevice = false, canUseRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
aButtonsState(canUseAnotherDevice = true, canUseRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
aButtonsState(canUseAnotherDevice = true, canUseRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
@ -51,8 +51,8 @@ fun aChooseSelfVerificationModeState(
fun aButtonsState(
canUseAnotherDevice: Boolean = true,
canEnterRecoveryKey: Boolean = true,
canUseRecoveryKey: Boolean = true,
) = ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
canUseRecoveryKey = canUseRecoveryKey,
)

View file

@ -122,10 +122,10 @@ private fun ChooseSelfVerificationModeButtons(
onClick = onUseAnotherDevice,
)
}
if (state.buttonsState.data.canEnterRecoveryKey) {
if (state.buttonsState.data.canUseRecoveryKey) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
text = stringResource(R.string.screen_identity_confirmation_use_recovery_key),
onClick = onUseRecoveryKey,
)
}

View file

@ -12,5 +12,4 @@
<string name="screen_identity_waiting_on_other_device">"Чаканне на іншай прыладзе…"</string>
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
<string name="screen_session_verification_enter_recovery_key">"Увядзіце ключ аднаўлення"</string>
</resources>

View file

@ -9,5 +9,4 @@
<string name="screen_identity_use_another_device">"Използване на друго устройство"</string>
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</string>
<string name="screen_notification_optin_title">"Разрешете известията и никога не пропускайте съобщение"</string>
<string name="screen_session_verification_enter_recovery_key">"Въвеждане на ключ за възстановяване"</string>
</resources>

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