mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 22:25:30 +00:00
Merge branch 'develop' into misc/reactions-pinned-messages
This commit is contained in:
@@ -69,18 +69,18 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
|
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
|
||||||
options:
|
options:
|
||||||
- 'Yes'
|
|
||||||
- 'No'
|
- 'No'
|
||||||
|
- 'Yes'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: pr
|
id: pr
|
||||||
attributes:
|
attributes:
|
||||||
label: Are you willing to provide a PR?
|
label: Will you submit a Pull Request?
|
||||||
description: |
|
description: |
|
||||||
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
||||||
options:
|
options:
|
||||||
- 'Yes'
|
|
||||||
- 'No'
|
- 'No'
|
||||||
|
- 'Yes'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Enhancement request
|
name: Enhancement request
|
||||||
description: Do you have a suggestion or feature request?
|
description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients.
|
||||||
labels: [T-Enhancement]
|
labels: [T-Enhancement]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
|
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: usecase
|
id: usecase
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ changelog:
|
|||||||
labels:
|
labels:
|
||||||
- PR-Dependencies
|
- PR-Dependencies
|
||||||
|
|
||||||
|
- title: 💼 Element Pro
|
||||||
|
labels:
|
||||||
|
- PR-Element-Pro
|
||||||
|
|
||||||
- title: Others
|
- title: Others
|
||||||
labels:
|
labels:
|
||||||
- PR-misc
|
- PR-misc
|
||||||
|
|||||||
@@ -44,19 +44,19 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APKs
|
- name: Assemble debug APKs
|
||||||
@@ -104,7 +104,18 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Compile release sources
|
- name: Compile release sources
|
||||||
if: ${{ matrix.variant == 'release' }}
|
if: ${{ matrix.variant == 'release' }}
|
||||||
run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: |
|
||||||
|
./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Run aapt dump
|
||||||
|
if: ${{ matrix.variant == 'release' }}
|
||||||
|
run: |
|
||||||
|
$ANDROID_HOME/build-tools/37.0.0/aapt dump badging \
|
||||||
|
./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \
|
||||||
|
> /tmp/currentAaptDump.txt
|
||||||
|
- name: Check manifest has not changed
|
||||||
|
if: ${{ matrix.variant == 'release' }}
|
||||||
|
run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt
|
||||||
- name: Compile nightly sources
|
- name: Compile nightly sources
|
||||||
if: ${{ matrix.variant == 'nightly' }}
|
if: ${{ matrix.variant == 'nightly' }}
|
||||||
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay Enterprise APK
|
- name: Assemble debug Gplay Enterprise APK
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
# Skip in forks, it doesn't work even with the fallback token
|
# Skip in forks, it doesn't work even with the fallback token
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ jobs:
|
|||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Run World screenshots generation script
|
- name: Run World screenshots generation script
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ jobs:
|
|||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -38,19 +38,19 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: maestro-test
|
group: maestro-test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
|
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
|
||||||
- name: Run Maestro tests in emulator
|
- name: Run Maestro tests in emulator
|
||||||
id: maestro_test
|
id: maestro_test
|
||||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
MAESTRO_USERNAME: maestroelement
|
MAESTRO_USERNAME: maestroelement
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ jobs:
|
|||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
|
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: false
|
cache-read-only: false
|
||||||
|
|
||||||
@@ -76,16 +76,16 @@ jobs:
|
|||||||
name: Dependency analysis
|
name: Dependency analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
@@ -51,11 +51,11 @@ jobs:
|
|||||||
name: Search for invalid screenshot files
|
name: Search for invalid screenshot files
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Search for invalid screenshot files
|
- name: Search for invalid screenshot files
|
||||||
@@ -65,20 +65,20 @@ jobs:
|
|||||||
name: Search for invalid dependencies
|
name: Search for invalid dependencies
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Search for invalid dependencies
|
- name: Search for invalid dependencies
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -108,12 +108,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Konsist tests
|
- name: Run Konsist tests
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -149,12 +149,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run compose tests
|
- name: Run compose tests
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -183,12 +183,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build Gplay Debug
|
||||||
@@ -213,7 +213,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -228,12 +228,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Detekt
|
- name: Run Detekt
|
||||||
@@ -254,7 +254,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -269,12 +269,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Ktlint check
|
- name: Run Ktlint check
|
||||||
@@ -295,7 +295,7 @@ jobs:
|
|||||||
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) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -319,7 +319,7 @@ jobs:
|
|||||||
name: Check shell scripts
|
name: Check shell scripts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run shellcheck
|
- name: Run shellcheck
|
||||||
@@ -333,10 +333,10 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
- uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||||
|
|
||||||
upload_reports:
|
upload_reports:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
@@ -344,7 +344,7 @@ jobs:
|
|||||||
needs: [konsist, lint, ktlint, detekt]
|
needs: [konsist, lint, ktlint, detekt]
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
@@ -82,12 +82,12 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create Enterprise app bundle
|
- name: Create Enterprise app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
@@ -122,16 +122,16 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create APKs
|
- name: Create APKs
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ else
|
|||||||
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
||||||
fi
|
fi
|
||||||
git add -A
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Screenshots are already up to date, nothing to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
git commit -m "Update screenshots"
|
git commit -m "Update screenshots"
|
||||||
|
|
||||||
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
||||||
|
|||||||
@@ -38,19 +38,19 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build debug code and test fixtures
|
- name: Build debug code and test fixtures
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||||
with:
|
with:
|
||||||
only-labels: "X-Needs-Info"
|
only-labels: "X-Needs-Info"
|
||||||
days-before-issue-stale: 30
|
days-before-issue-stale: 30
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ jobs:
|
|||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Setup Localazy
|
- name: Setup Localazy
|
||||||
@@ -39,6 +39,7 @@ jobs:
|
|||||||
./tools/localazy/downloadStrings.sh --all
|
./tools/localazy/downloadStrings.sh --all
|
||||||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||||
./tools/test/generateAllScreenshots.py
|
./tools/test/generateAllScreenshots.py
|
||||||
|
./tools/manifest/aaptDump.sh
|
||||||
- name: Create Pull Request for Strings
|
- name: Create Pull Request for Strings
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Install Prerequisite dependencies
|
- name: Install Prerequisite dependencies
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ permissions: {}
|
|||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
env:
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
|
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -63,17 +63,23 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
|
- name: ⚙️ Run unit tests
|
||||||
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:testGplayDebugUnitTest testDebugUnitTest -x :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
|
- name: ⚙️ Run screenshot tests
|
||||||
|
run: ./gradlew :tests:uitests:verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
|
- name: ⚙️ Generate kover coverage report
|
||||||
|
run: ./gradlew :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll -x testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
- name: 🚫 Upload kover failed coverage reports
|
- name: 🚫 Upload kover failed coverage reports
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -81,7 +87,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: kover-error-report
|
name: kover-error-report
|
||||||
path: |
|
path: |
|
||||||
app/build/reports/kover
|
build/reports/kover
|
||||||
|
|
||||||
- name: ✅ Upload kover report (disabled)
|
- name: ✅ Upload kover report (disabled)
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
Generated
+34
@@ -1,5 +1,39 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="android" withSubpackages="true" static="true" />
|
||||||
|
<package name="androidx" withSubpackages="true" static="true" />
|
||||||
|
<package name="com" withSubpackages="true" static="true" />
|
||||||
|
<package name="junit" withSubpackages="true" static="true" />
|
||||||
|
<package name="net" withSubpackages="true" static="true" />
|
||||||
|
<package name="org" withSubpackages="true" static="true" />
|
||||||
|
<package name="java" withSubpackages="true" static="true" />
|
||||||
|
<package name="javax" withSubpackages="true" static="true" />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="androidx" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="junit" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="net" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="org" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
|||||||
Generated
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="externalSystemId" value="Gradle" />
|
<option name="externalSystemId" value="Gradle" />
|
||||||
<option name="version" value="2.4.0" />
|
<option name="version" value="2.4.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -16,4 +16,4 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Device verified"
|
visible: "Device verified"
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
- tapOn: "Continue"
|
- tapOn: "Done"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
appId: ${MAESTRO_APP_ID}
|
||||||
|
---
|
||||||
|
# Clean up the room list if it was left in a bad state by previous tests
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'aRoomName'
|
||||||
|
commands:
|
||||||
|
- longPressOn: 'aRoomName'
|
||||||
|
- tapOn:
|
||||||
|
text: "Leave room"
|
||||||
|
index: 0
|
||||||
|
- tapOn: "Leave"
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: ${MAESTRO_INVITEE1_MXID}
|
||||||
|
commands:
|
||||||
|
- longPressOn: ${MAESTRO_INVITEE1_MXID}
|
||||||
|
- tapOn:
|
||||||
|
text: "Leave room"
|
||||||
|
index: 0
|
||||||
|
- tapOn: "Leave"
|
||||||
@@ -7,7 +7,16 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- tapOn:
|
- tapOn:
|
||||||
text: ${MAESTRO_INVITEE1_MXID}
|
text: ${MAESTRO_INVITEE1_MXID}
|
||||||
index: 1
|
index: 1
|
||||||
- tapOn: "Continue"
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'Start a chat with this new contact?'
|
||||||
|
commands:
|
||||||
|
- tapOn: 'Continue'
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'Send invite?'
|
||||||
|
commands:
|
||||||
|
- tapOn: 'Send invite'
|
||||||
- takeScreenshot: build/maestro/330-createAndDeleteDM
|
- takeScreenshot: build/maestro/330-createAndDeleteDM
|
||||||
- tapOn: "maestroelement2"
|
- tapOn: "maestroelement2"
|
||||||
- scroll
|
- scroll
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
text: ${MAESTRO_INVITEE1_MXID}
|
text: ${MAESTRO_INVITEE1_MXID}
|
||||||
index: 1
|
index: 1
|
||||||
- tapOn: "Finish"
|
- tapOn: "Finish"
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'Invite new contact to this room?'
|
||||||
|
commands:
|
||||||
|
- tapOn:
|
||||||
|
id: "confirm_invite_unknown"
|
||||||
- tapOn: "aRoomName"
|
- tapOn: "aRoomName"
|
||||||
- tapOn: "Invite"
|
- tapOn: "Invite"
|
||||||
# assert there's 1 member and 1 invitee
|
# assert there's 1 member and 1 invitee
|
||||||
@@ -23,6 +29,10 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- tapOn:
|
- tapOn:
|
||||||
text: ${MAESTRO_INVITEE2_MXID}
|
text: ${MAESTRO_INVITEE2_MXID}
|
||||||
index: 1
|
index: 1
|
||||||
|
# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press
|
||||||
|
- tapOn:
|
||||||
|
# Navigation title
|
||||||
|
text: "Invite people"
|
||||||
- tapOn: "Invite"
|
- tapOn: "Invite"
|
||||||
- runFlow:
|
- runFlow:
|
||||||
when:
|
when:
|
||||||
@@ -30,10 +40,6 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
commands:
|
commands:
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "confirm_invite_unknown"
|
id: "confirm_invite_unknown"
|
||||||
# Close the keyboard if it's still open
|
|
||||||
- tapOn: "Back"
|
|
||||||
# Go back to the room details screen
|
|
||||||
- tapOn: "Back"
|
|
||||||
- scrollUntilVisible:
|
- scrollUntilVisible:
|
||||||
direction: DOWN
|
direction: DOWN
|
||||||
element:
|
element:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
|
- runFlow: cleanUpRoomList.yaml
|
||||||
- runFlow: searchRoomList.yaml
|
- runFlow: searchRoomList.yaml
|
||||||
- takeScreenshot: build/maestro/300-RoomList
|
- takeScreenshot: build/maestro/300-RoomList
|
||||||
- runFlow: timeline/timeline.yaml
|
- runFlow: timeline/timeline.yaml
|
||||||
|
|||||||
+245
@@ -1,3 +1,248 @@
|
|||||||
|
Changes in Element X v26.08.1
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
|
||||||
|
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
|
||||||
|
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
|
||||||
|
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
|
||||||
|
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
|
||||||
|
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
|
||||||
|
### 🧱 Build
|
||||||
|
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
|
||||||
|
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
|
||||||
|
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
|
||||||
|
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
|
||||||
|
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
|
||||||
|
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
|
||||||
|
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
|
||||||
|
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
|
||||||
|
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
|
||||||
|
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
|
||||||
|
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
|
||||||
|
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
|
||||||
|
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
|
||||||
|
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
|
||||||
|
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
|
||||||
|
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
|
||||||
|
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
|
||||||
|
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
|
||||||
|
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
|
||||||
|
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
|
||||||
|
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
|
||||||
|
### 💼 Element Pro
|
||||||
|
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
|
||||||
|
### Others
|
||||||
|
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
|
||||||
|
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
|
||||||
|
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
|
||||||
|
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
|
||||||
|
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
|
||||||
|
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
|
||||||
|
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
|
||||||
|
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
|
||||||
|
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
|
||||||
|
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
|
||||||
|
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
|
||||||
|
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
|
||||||
|
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
|
||||||
|
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
|
||||||
|
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
|
||||||
|
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
|
||||||
|
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
|
||||||
|
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
|
||||||
|
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
|
||||||
|
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
|
||||||
|
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
|
||||||
|
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
|
||||||
|
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
|
||||||
|
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
|
||||||
|
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
|
||||||
|
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
|
||||||
|
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
|
||||||
|
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
|
||||||
|
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
|
||||||
|
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
|
||||||
|
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
|
||||||
|
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
|
||||||
|
|
||||||
|
Changes in Element X v26.08.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.08.0 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* feat(call): Active call timeline rendering by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/7051
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Announce expanded/collapsed state on the timeline group header by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7226
|
||||||
|
* Allow adding extra elements to the developer settings from external modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7230
|
||||||
|
* Update keyboard visibility to track hasFocus by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7284
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Live location sharing timeout by @ganfra in https://github.com/element-hq/element-x-android/pull/7184
|
||||||
|
* Fix logging via QR code when using MDM-configured homeserver by @mxandreas in https://github.com/element-hq/element-x-android/pull/7228
|
||||||
|
* Fix #7181: Remove back arrow from device verified screen and use 'Done' text by @thirumani-vihaan in https://github.com/element-hq/element-x-android/pull/7222
|
||||||
|
* Fix padding for captions of media in 'not found' state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7247
|
||||||
|
* Fix banned mime type errors in `DefaultContentScannerService` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7255
|
||||||
|
* Fix `ContentErrorView` not calculating its text size synchronously by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7281
|
||||||
|
* Fix media viewer for medias not related to events displaying invalid state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7280
|
||||||
|
* Try making `HeaderFooterView` more robust by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7267
|
||||||
|
* Fix insets in several screens by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7285
|
||||||
|
### ⚠️ API Changes
|
||||||
|
* Add a password strength estimator wrapping the SDK zxcvbn API by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7186
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7277
|
||||||
|
### 🧱 Build
|
||||||
|
* CI: fix 'Update Gradle Wrapper' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7238
|
||||||
|
* Update Gradle Wrapper from 9.5.1 to 9.6.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7240
|
||||||
|
* Localazy: improve search, parallelize checks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7261
|
||||||
|
* Run `Konsist` tests sequentially by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7293
|
||||||
|
### 📄 Documentation
|
||||||
|
* Group Element Pro changes into a dedicated release notes section by @manuroe in https://github.com/element-hq/element-x-android/pull/7236
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Plug emoji picker for custom user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7257
|
||||||
|
* WIP : display user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7232
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update roborazzi to v1.67.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7188
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.48.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7217
|
||||||
|
* Update dependency com.google.crypto.tink:tink-android to v1.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7229
|
||||||
|
* Update kotlin to v2.3.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7234
|
||||||
|
* Update dependency net.zetetic:sqlcipher-android to v4.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7218
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v34.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7237
|
||||||
|
* Update dependencyAnalysis to v3.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7212
|
||||||
|
* Update actions/setup-java action to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7214
|
||||||
|
* Update dependency io.element.android:element-call-embedded to v0.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7161
|
||||||
|
* Update kotlin to v2.4.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7244
|
||||||
|
* Update roborazzi to v1.68.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7245
|
||||||
|
* Update metro to v1.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7243
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7258
|
||||||
|
* Update zizmorcore/zizmor-action action to v0.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7256
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7262
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.49.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7263
|
||||||
|
* Update dependencyAnalysis to v3.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7265
|
||||||
|
* Update actions/setup-java action to v5.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7268
|
||||||
|
* Update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7273
|
||||||
|
* Update actions/setup-python action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7278
|
||||||
|
* Update roborazzi to v1.70.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7274
|
||||||
|
* Update actions/checkout action to v7.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7282
|
||||||
|
* Import Compound tokens from `v10.2.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7272
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7287
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v13.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7290
|
||||||
|
### 💼 Element Pro
|
||||||
|
* Content scanner (Element Pro): scan timeline events for unsafe content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7159
|
||||||
|
* Add content scanner support to media gallery and viewer (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7233
|
||||||
|
* Enforce HS disabling E2EE in DM creation too (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7292
|
||||||
|
* Allow adding a path suffix to the OAuth client URI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7286
|
||||||
|
### Others
|
||||||
|
* [A11y] Improve rendering on focused UI items by @bmarty in https://github.com/element-hq/element-x-android/pull/7182
|
||||||
|
* Add a button role to clickable icons and text on the login/onboarding screens by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7225
|
||||||
|
* Expose formatting toggle state to TalkBack by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7227
|
||||||
|
* Add warning to the 'Scan QR code' screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7264
|
||||||
|
* Refactor SearchBarResultState.Initial/NoResultsFound to data objects by @ganfra in https://github.com/element-hq/element-x-android/pull/7266
|
||||||
|
* Remove no longer temporary strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7260
|
||||||
|
* Use the right insets in the home screen UI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7271
|
||||||
|
* Make settings section separators visible in black theme by @mvanhorn in https://github.com/element-hq/element-x-android/pull/7039
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @thirumani-vihaan made their first contribution in https://github.com/element-hq/element-x-android/pull/7222
|
||||||
|
* @mvanhorn made their first contribution in https://github.com/element-hq/element-x-android/pull/7039
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.1...v26.08.0
|
||||||
|
|
||||||
|
Changes in Element X v26.07.1
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.07.1 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Scroll to unread messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7024
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Collapse runs of deleted messages in the timeline by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7195
|
||||||
|
* Allow homeserver to disable encryption in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7198
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix jump-to-unread requiring two taps for an out-of-window read marker by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6943
|
||||||
|
* Add content description to video preview for screen readers by @bmarty in https://github.com/element-hq/element-x-android/pull/7137
|
||||||
|
* Fix accessibility for message composer input and read receipts button by @bmarty in https://github.com/element-hq/element-x-android/pull/7135
|
||||||
|
* Add accessibility label to search input by @bmarty in https://github.com/element-hq/element-x-android/pull/7133
|
||||||
|
* Fix timeline reading order distorted when TalkBack is active by @bmarty in https://github.com/element-hq/element-x-android/pull/7138
|
||||||
|
* Add programmatic label and focus indicator to PIN entry field by @bmarty in https://github.com/element-hq/element-x-android/pull/7132
|
||||||
|
* Fix timelines not loading after marking a room as read from notification when app was killed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7202
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7192
|
||||||
|
### 🧱 Build
|
||||||
|
* Bump the default Gradle daemon heap to match what CI already uses by @PolynomialDivision in https://github.com/element-hq/element-x-android/pull/7152
|
||||||
|
* Update screenshots by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7155
|
||||||
|
* Update GitHub templates by @bmarty in https://github.com/element-hq/element-x-android/pull/7170
|
||||||
|
* Add check on merged manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/7172
|
||||||
|
* Fix AAPT dump missing and reordering some permissions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7193
|
||||||
|
* Try fixing DM creation in Maestro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7204
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Support gallery messages by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6519
|
||||||
|
* Support gallery messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7112
|
||||||
|
* Initial content scanner integration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7139
|
||||||
|
* WIP : Set user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7094
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.44.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7093
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v13.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7129
|
||||||
|
* Update actions/setup-java action to v5.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7143
|
||||||
|
* Update danger/danger-js action to v13.0.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7142
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.46.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7140
|
||||||
|
* Update dependencyAnalysis to v3.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7144
|
||||||
|
* Update plugin gms_google_services to v4.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7086
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7007
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7158
|
||||||
|
* Update metro to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7168
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7178
|
||||||
|
* Update dependency androidx.compose:compose-bom to v2026.06.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7174
|
||||||
|
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7180
|
||||||
|
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7201
|
||||||
|
* Update dependency io.github.zxing-cpp:android to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7206
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7194
|
||||||
|
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7177
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7216
|
||||||
|
### Others
|
||||||
|
* Timeline: jump-to-unread FAB and mark-as-read shortcut by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6694
|
||||||
|
* Iterate on visual list by @bmarty in https://github.com/element-hq/element-x-android/pull/7141
|
||||||
|
* Fix wrong avatar shown in the thread list's top bar by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7044
|
||||||
|
* Update onboarding screen text style by @bmarty in https://github.com/element-hq/element-x-android/pull/7157
|
||||||
|
* Add full SVG support by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7020
|
||||||
|
* Add skin tone picker to the emoji picker by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6917
|
||||||
|
* Fix the message edit being lost when attaching media by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7090
|
||||||
|
* Cleanup AndroidManifest.xml files. by @bmarty in https://github.com/element-hq/element-x-android/pull/7171
|
||||||
|
* Handle local network permission - Android 17 by @ganfra in https://github.com/element-hq/element-x-android/pull/7165
|
||||||
|
* Add item count text as the placeholder for in reply to box for galleries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7176
|
||||||
|
* Default to dark theme for EC by @toger5 in https://github.com/element-hq/element-x-android/pull/7162
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @PolynomialDivision made their first contribution in https://github.com/element-hq/element-x-android/pull/7152
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.0...v26.07.1
|
||||||
|
|
||||||
Changes in Element X v26.07.0
|
Changes in Element X v26.07.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- To be able to install APK from the application -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ElementXApplication"
|
android:name=".ElementXApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import io.element.android.libraries.di.BaseDirectory
|
|||||||
import io.element.android.libraries.di.CacheDirectory
|
import io.element.android.libraries.di.CacheDirectory
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
|
|
||||||
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
|
|
||||||
import io.element.android.x.BuildConfig
|
import io.element.android.x.BuildConfig
|
||||||
import io.element.android.x.R
|
import io.element.android.x.R
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
@@ -115,10 +113,4 @@ object AppModule {
|
|||||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
||||||
return SnackbarDispatcher()
|
return SnackbarDispatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
|
||||||
return DefaultEmojibaseProvider(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultSessionGraphFactory(
|
class DefaultSessionGraphFactory(
|
||||||
private val appGraph: AppGraph
|
private val appGraph: AppGraph,
|
||||||
) : SessionGraphFactory {
|
) : SessionGraphFactory {
|
||||||
override fun create(client: MatrixClient): Any {
|
override fun create(client: MatrixClient): Any {
|
||||||
return appGraph.sessionGraphFactory.create(client)
|
return appGraph.sessionGraphFactory.create(client)
|
||||||
|
|||||||
@@ -35,4 +35,11 @@ object RageshakeConfig {
|
|||||||
* The maximum number of log lines a rageshake can contain.
|
* The maximum number of log lines a rageshake can contain.
|
||||||
*/
|
*/
|
||||||
const val MAX_LOG_LINES_SIZE = 1_000_000
|
const val MAX_LOG_LINES_SIZE = 1_000_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
|
||||||
|
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
|
||||||
|
* mainly the list of the omitted log file names.
|
||||||
|
*/
|
||||||
|
const val MAX_DESCRIPTION_SIZE = 40 * 1024
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.oauth.api)
|
implementation(projects.libraries.oauth.api)
|
||||||
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
@@ -59,8 +60,10 @@ dependencies {
|
|||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.features.login.test)
|
testImplementation(projects.features.login.test)
|
||||||
testImplementation(projects.features.share.test)
|
testImplementation(projects.features.share.test)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.oauth.test)
|
testImplementation(projects.libraries.oauth.test)
|
||||||
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ sealed interface LoggedInEvents {
|
|||||||
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
||||||
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
||||||
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
||||||
|
data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
|
||||||
|
|
||||||
|
data object RequestLocationNetworkPermission : LoggedInEvents
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -28,6 +30,8 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
@@ -36,6 +40,10 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
|||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
@@ -58,7 +66,13 @@ class LoggedInPresenter(
|
|||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
|
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||||
|
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -109,6 +123,16 @@ class LoggedInPresenter(
|
|||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// Keep automatic call status (m.call) in sync with the feature flag and homeserver support.
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
||||||
|
.onEach { isUserStatusEnabled ->
|
||||||
|
val enabled = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
|
matrixClient.enableAutomaticCallStatus(enabled)
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
||||||
LaunchedEffect(networkConnectivity) {
|
LaunchedEffect(networkConnectivity) {
|
||||||
if (networkConnectivity == NetworkStatus.Connected) {
|
if (networkConnectivity == NetworkStatus.Connected) {
|
||||||
@@ -117,6 +141,21 @@ class LoggedInPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val localNetworkPermissionState = localNetworkPermissionsPresenter.present()
|
||||||
|
var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) }
|
||||||
|
val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) {
|
||||||
|
value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl)
|
||||||
|
}
|
||||||
|
LaunchedEffect(localNetworkPermissionState.permissionGranted) {
|
||||||
|
if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false
|
||||||
|
}
|
||||||
|
val localNetworkPermissionDialog = when {
|
||||||
|
!advisorRequestsPrompt -> LocalNetworkPermissionDialog.None
|
||||||
|
localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None
|
||||||
|
localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale
|
||||||
|
else -> LocalNetworkPermissionDialog.Settings
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvent(event: LoggedInEvents) {
|
fun handleEvent(event: LoggedInEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is LoggedInEvents.CloseErrorDialog -> {
|
is LoggedInEvents.CloseErrorDialog -> {
|
||||||
@@ -134,6 +173,17 @@ class LoggedInPresenter(
|
|||||||
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
||||||
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
||||||
}
|
}
|
||||||
|
LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
|
||||||
|
localNetworkPromptDismissedThisSession = true
|
||||||
|
}
|
||||||
|
LoggedInEvents.RequestLocationNetworkPermission -> {
|
||||||
|
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
|
||||||
|
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||||
|
} else {
|
||||||
|
localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions)
|
||||||
|
}
|
||||||
|
localNetworkPromptDismissedThisSession = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +193,7 @@ class LoggedInPresenter(
|
|||||||
ignoreRegistrationError = ignoreRegistrationError,
|
ignoreRegistrationError = ignoreRegistrationError,
|
||||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
data class LoggedInState(
|
data class LoggedInState(
|
||||||
val showSyncSpinner: Boolean,
|
val showSyncSpinner: Boolean,
|
||||||
@@ -16,5 +17,6 @@ data class LoggedInState(
|
|||||||
val ignoreRegistrationError: Boolean,
|
val ignoreRegistrationError: Boolean,
|
||||||
val forceNativeSlidingSyncMigration: Boolean,
|
val forceNativeSlidingSyncMigration: Boolean,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
val eventSink: (LoggedInEvents) -> Unit,
|
val eventSink: (LoggedInEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
|
|
||||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||||
@@ -19,6 +20,8 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
|||||||
aLoggedInState(showSyncSpinner = true),
|
aLoggedInState(showSyncSpinner = true),
|
||||||
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
||||||
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
||||||
|
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||||
|
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +30,13 @@ fun aLoggedInState(
|
|||||||
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
forceNativeSlidingSyncMigration: Boolean = false,
|
forceNativeSlidingSyncMigration: Boolean = false,
|
||||||
appName: String = "Element X",
|
appName: String = "Element X",
|
||||||
|
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||||
) = LoggedInState(
|
) = LoggedInState(
|
||||||
showSyncSpinner = showSyncSpinner,
|
showSyncSpinner = showSyncSpinner,
|
||||||
pusherRegistrationState = pusherRegistrationState,
|
pusherRegistrationState = pusherRegistrationState,
|
||||||
ignoreRegistrationError = false,
|
ignoreRegistrationError = false,
|
||||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||||
appName = appName,
|
appName = appName,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@@ -82,6 +83,12 @@ fun LoggedInView(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.localNetworkPermissionDialog,
|
||||||
|
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
|
||||||
|
onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.getReason(): String? {
|
private fun Throwable.getReason(): String? {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import im.vector.app.features.analytics.plan.UserProperties
|
|||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
@@ -36,6 +39,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
@@ -46,6 +51,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -124,6 +130,9 @@ class LoggedInPresenterTest {
|
|||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
).test {
|
).test {
|
||||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
@@ -342,6 +351,57 @@ class LoggedInPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - does not enable automatic call status when server does not support it`() = runTest {
|
||||||
|
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
|
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
|
||||||
|
).apply {
|
||||||
|
isUserStatusSupportedResult = Result.success(false)
|
||||||
|
}
|
||||||
|
createLoggedInPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.UserStatus.key to true),
|
||||||
|
),
|
||||||
|
).test {
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
assert(enableAutomaticCallStatusLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - reacts to user status flag being toggled at runtime`() = runTest {
|
||||||
|
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
|
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
|
||||||
|
).apply {
|
||||||
|
isUserStatusSupportedResult = Result.success(true)
|
||||||
|
}
|
||||||
|
val featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.UserStatus.key to false),
|
||||||
|
)
|
||||||
|
createLoggedInPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
|
).test {
|
||||||
|
awaitItem()
|
||||||
|
// Turning the flag on at runtime enables it.
|
||||||
|
featureFlagService.setFeatureEnabled(FeatureFlags.UserStatus, true)
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
assert(enableAutomaticCallStatusLambda)
|
||||||
|
.isCalledExactly(2)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value(false)),
|
||||||
|
listOf(value(true)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
@@ -358,6 +418,7 @@ class LoggedInPresenterTest {
|
|||||||
),
|
),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
@@ -368,6 +429,9 @@ class LoggedInPresenterTest {
|
|||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ Follow these steps to install and configure the plugin and templates:
|
|||||||
- Click on OK
|
- Click on OK
|
||||||
4. Configure generate-module-from-template plugin :
|
4. Configure generate-module-from-template plugin :
|
||||||
- Navigate to AS/Settings/Tools/Module Template Settings
|
- Navigate to AS/Settings/Tools/Module Template Settings
|
||||||
|
- If a `FeatureModule` template is already listed, select it and remove it, since the plugin keeps its own copy of the
|
||||||
|
imported template and will not pick up changes made to `FeatureModule.json` on its own
|
||||||
- Click on + / Import From File
|
- Click on + / Import From File
|
||||||
- Pick the `tools/templates/FeatureModule.json`
|
- Pick the `tools/templates/FeatureModule.json`
|
||||||
|
|
||||||
@@ -321,8 +323,10 @@ Example for a new feature called RoomDetails:
|
|||||||
5. The modules api/impl should be created under `features/roomdetails` directory.
|
5. The modules api/impl should be created under `features/roomdetails` directory.
|
||||||
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
|
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
|
||||||
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`.
|
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`.
|
||||||
To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`.
|
To use it, just right click on the package where you want to generate classes, and click on `New` / `Template Presentation Classes`.
|
||||||
Fill the text field with the base name of the classes, ie `RootRoomDetails` in the `root` package.
|
Fill the text field with the base name of the classes: the generated files are `<BaseName>Event.kt`, `<BaseName>State.kt`,
|
||||||
|
`<BaseName>StateProvider.kt`, `<BaseName>Presenter.kt`, `<BaseName>Node.kt` and `<BaseName>View.kt`. For instance
|
||||||
|
`PreferencesRoot` in the `features/preferences/impl/root` package.
|
||||||
|
|
||||||
|
|
||||||
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
|
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
|
||||||
|
|||||||
+1
-1
Submodule enterprise updated: efdf9d9a15...f5f910856c
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements, including fixing the local network permission usage in Android 17.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
+1
-1
@@ -39,7 +39,7 @@ fun AnalyticsPreferencesView(
|
|||||||
)
|
)
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(stringResource(id = R.string.screen_analytics_settings_share_data))
|
Text(stringResource(id = R.string.screen_analytics_settings_share_data))
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
android:name="android.hardware.microphone"
|
android:name="android.hardware.microphone"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|||||||
+1
-2
@@ -22,7 +22,6 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.call.api.CallData
|
import io.element.android.features.call.api.CallData
|
||||||
import io.element.android.features.call.impl.data.WidgetMessage
|
import io.element.android.features.call.impl.data.WidgetMessage
|
||||||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||||
@@ -84,7 +83,7 @@ class CallScreenPresenter(
|
|||||||
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
|
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
|
||||||
var webViewError by remember { mutableStateOf<String?>(null) }
|
var webViewError by remember { mutableStateOf<String?>(null) }
|
||||||
val languageTag = languageTagProvider.provideLanguageTag()
|
val languageTag = languageTagProvider.provideLanguageTag()
|
||||||
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
|
val theme = "dark"
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|||||||
+21
-16
@@ -38,6 +38,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
|
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||||
import io.element.android.features.call.api.CallData
|
import io.element.android.features.call.api.CallData
|
||||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||||
import io.element.android.features.call.impl.di.CallBindings
|
import io.element.android.features.call.impl.di.CallBindings
|
||||||
@@ -143,24 +144,28 @@ class ElementCallActivity :
|
|||||||
compoundDark = colors.dark,
|
compoundDark = colors.dark,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
) {
|
) {
|
||||||
val state = presenter.present()
|
ForcedDarkElementTheme(
|
||||||
eventSink = state.eventSink
|
colors = colors,
|
||||||
LaunchedEffect(state.isCallActive) {
|
) {
|
||||||
if (state.isCallActive) {
|
val state = presenter.present()
|
||||||
setCallIsActive()
|
eventSink = state.eventSink
|
||||||
|
LaunchedEffect(state.isCallActive) {
|
||||||
|
if (state.isCallActive) {
|
||||||
|
setCallIsActive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
CallScreenView(
|
||||||
|
state = state,
|
||||||
|
pipState = pipState,
|
||||||
|
onConsoleMessage = {
|
||||||
|
consoleMessageLogger.log("ElementCall", it)
|
||||||
|
},
|
||||||
|
requestPermissions = { permissions, callback ->
|
||||||
|
requestPermissionCallback = callback
|
||||||
|
requestPermissionsLauncher.launch(permissions)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
CallScreenView(
|
|
||||||
state = state,
|
|
||||||
pipState = pipState,
|
|
||||||
onConsoleMessage = {
|
|
||||||
consoleMessageLogger.log("ElementCall", it)
|
|
||||||
},
|
|
||||||
requestPermissions = { permissions, callback ->
|
|
||||||
requestPermissionCallback = callback
|
|
||||||
requestPermissionsLauncher.launch(permissions)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.contentscanner.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to perform security scans in the contents for a given media source.
|
||||||
|
*
|
||||||
|
* The default implementation (FOSS) always returns a valid state.
|
||||||
|
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
||||||
|
*/
|
||||||
|
fun interface ContentScannerService {
|
||||||
|
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
||||||
|
}
|
||||||
+6
-7
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
@@ -14,19 +13,19 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.recentemojis.impl"
|
namespace = "io.element.android.features.contentscanner.impl"
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
setupDependencyInjection()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.libraries.recentemojis.api)
|
api(projects.features.contentscanner.api)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(libs.matrix.emojibase.bindings)
|
implementation(projects.libraries.designsystem)
|
||||||
|
|
||||||
testImplementation(projects.libraries.recentemojis.test)
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl
|
||||||
|
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [ContentScannerService] that uses a [ContentScanner] to scan media sources.
|
||||||
|
*/
|
||||||
|
class DefaultContentScannerService(
|
||||||
|
private val contentScanner: ContentScanner,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : ContentScannerService {
|
||||||
|
private val dispatcher = coroutineDispatchers.io.limitedParallelism(4)
|
||||||
|
|
||||||
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
|
for (mediaSource in mediaSources) {
|
||||||
|
val url = mediaSource.safeUrl
|
||||||
|
val currentState = contentValidationState.getCurrentMediaState(url)
|
||||||
|
if (currentState != ContentValidationValue.Unknown) continue
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Loading)
|
||||||
|
|
||||||
|
coroutineScope.launch(dispatcher) {
|
||||||
|
contentScanner.scan(mediaSource)
|
||||||
|
.onSuccess { isValid ->
|
||||||
|
val contentValidationValue = if (isValid) ContentValidationValue.Valid else ContentValidationValue.Invalid
|
||||||
|
contentValidationState.update(url, contentValidationValue)
|
||||||
|
}
|
||||||
|
.onFailure { exception ->
|
||||||
|
when (exception) {
|
||||||
|
is IOException -> {
|
||||||
|
// If it's an IO-related exception, we can retry later, so we don't store the error
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
is ClientException.ContentScanner if exception.reason.isDangerous() -> {
|
||||||
|
// It's a dangerous content (banned mime-type?), we mark it as invalid
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// For other exceptions, we cache the failure
|
||||||
|
contentValidationState.update(url, ContentValidationValue.UnrecoverableError(exception))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
@ContributesBinding(RoomScope::class)
|
||||||
|
class DefaultEventContentValidationCache(
|
||||||
|
private val contentScanner: ContentScanner?,
|
||||||
|
) : EventContentValidationCache {
|
||||||
|
private val cache = ConcurrentHashMap<EventId, ContentValidationState>()
|
||||||
|
|
||||||
|
override operator fun get(eventId: EventId): ContentValidationState {
|
||||||
|
return cache.getOrPut(eventId) {
|
||||||
|
if (contentScanner != null) {
|
||||||
|
DefaultContentValidationState()
|
||||||
|
} else {
|
||||||
|
Timber.v("Content scanner is not available, returning NoopContentValidationState for eventId: $eventId")
|
||||||
|
NoopContentValidationState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl.di
|
||||||
|
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noop implementation of [ContentScannerService] that always returns success.
|
||||||
|
*
|
||||||
|
* This is used when the content scanner feature is not enabled or available.
|
||||||
|
*/
|
||||||
|
class AlwaysValidContentScannerService : ContentScannerService {
|
||||||
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
|
// Always return success for the noop implementation
|
||||||
|
for (mediaSource in mediaSources) {
|
||||||
|
val url = mediaSource.safeUrl
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl.di
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.BindingContainer
|
||||||
|
import dev.zacsweers.metro.ContributesTo
|
||||||
|
import dev.zacsweers.metro.Provides
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.features.contentscanner.impl.DefaultContentScannerService
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.annotations.RoomCoroutineScope
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
@BindingContainer
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
object ContentScannerModule {
|
||||||
|
@Provides
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
fun providesContentScannerService(
|
||||||
|
contentScanner: ContentScanner?,
|
||||||
|
@RoomCoroutineScope coroutineScope: CoroutineScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
): ContentScannerService {
|
||||||
|
return if (contentScanner != null) {
|
||||||
|
DefaultContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AlwaysValidContentScannerService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class DefaultContentScannerServiceTest {
|
||||||
|
@Test
|
||||||
|
fun `scan with no media sources returns unknown`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(true) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = emptyList(),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
scanLambda.assertions().isNeverCalled()
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan with single media source`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan with several media sources`() = runTest {
|
||||||
|
val mediaSourceA = aMediaSource("https://example.com/mediaA.jpg")
|
||||||
|
val mediaSourceB = aMediaSource("https://example.com/mediaB.jpg")
|
||||||
|
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { source ->
|
||||||
|
when (source) {
|
||||||
|
mediaSourceA -> Result.success(true)
|
||||||
|
mediaSourceB -> Result.success(false)
|
||||||
|
else -> error("Unexpected media source: $source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = listOf(mediaSourceA, mediaSourceB),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledExactly(2)
|
||||||
|
assertThat(contentValidationState.getMediaStateFlow(mediaSourceA.safeUrl).first()).isEqualTo(ContentValidationValue.Valid)
|
||||||
|
assertThat(contentValidationState.getMediaStateFlow(mediaSourceB.safeUrl).first()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
assertThat(contentValidationState.overallStateFlow.first()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan won't be repeated for sources which are already being scanned`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
// First scan attempt
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Immediately after, a new scan attempt is made for the same media source
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// The scan action was done just once, and the overall state is invalid as expected
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan won't be repeated for sources which already have a cached value`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = NoopContentValidationState(ContentValidationValue.Valid)
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if a scan fails with a recoverable error, the content validation value will be back to Unknown`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.failure(FileNotFoundException("Some IO exception")) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentMediaState(mediaSource.safeUrl)).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if a scan fails with an unrecoverable error, the content validation value will be back to UnrecoverableError`() = runTest {
|
||||||
|
val error = OutOfMemoryError("BOOM")
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.failure(error) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentMediaState(mediaSource.safeUrl)).isEqualTo(ContentValidationValue.UnrecoverableError(error))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.UnrecoverableError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createDefaultContentScannerService(
|
||||||
|
contentScanner: ContentScanner = FakeContentScanner(),
|
||||||
|
sessionCoroutineScope: CoroutineScope = backgroundScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
): DefaultContentScannerService {
|
||||||
|
return DefaultContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultEventContentValidationCacheTest {
|
||||||
|
@Test
|
||||||
|
fun `get returns NoopContentValidationState when ContentScanner is null`() {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = null
|
||||||
|
val cache = DefaultEventContentValidationCache(contentScanner)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val state = cache[AN_EVENT_ID]
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(state).isInstanceOf(NoopContentValidationState::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get returns an actual DefaultContentValidationState when ContentScanner is not null`() {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
|
val cache = DefaultEventContentValidationCache(contentScanner)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val state = cache[AN_EVENT_ID]
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(state).isInstanceOf(DefaultContentValidationState::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.contentscanner.impl.di
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.contentscanner.impl.DefaultContentScannerService
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ContentScannerModuleTest {
|
||||||
|
@Test
|
||||||
|
fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = null
|
||||||
|
val sessionCoroutineScope = backgroundScope
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(contentScannerService).isInstanceOf(AlwaysValidContentScannerService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
|
val sessionCoroutineScope = backgroundScope
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(contentScannerService).isInstanceOf(DefaultContentScannerService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ dependencies {
|
|||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.features.invitepeople.api)
|
implementation(projects.features.invitepeople.api)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
@@ -53,4 +54,5 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.features.startchat.test)
|
testImplementation(projects.features.startchat.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
testImplementation(projects.features.enterprise.test)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -24,6 +24,7 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
@@ -73,6 +74,7 @@ class ConfigureRoomPresenter(
|
|||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val roomAliasHelper: RoomAliasHelper,
|
private val roomAliasHelper: RoomAliasHelper,
|
||||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||||
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@@ -256,7 +258,7 @@ class ConfigureRoomPresenter(
|
|||||||
CreateRoomParameters(
|
CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
isEncrypted = true,
|
isEncrypted = !sessionEnterpriseService.isEncryptionDisabledByHomeserver(),
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
visibility = RoomVisibility.Private,
|
visibility = RoomVisibility.Private,
|
||||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||||
|
|||||||
+1
-1
@@ -312,7 +312,7 @@ private fun RoomJoinRuleOptions(
|
|||||||
backgroundTint = Color.Transparent,
|
backgroundTint = Color.Transparent,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = {
|
content = {
|
||||||
val title = when (item) {
|
val title = when (item) {
|
||||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||||
is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||||
|
|||||||
+3
-3
@@ -58,7 +58,7 @@ internal fun SelectParentSpaceOptions(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = selectedSpace?.displayName
|
text = selectedSpace?.displayName
|
||||||
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||||
@@ -126,7 +126,7 @@ private fun SelectParentSpaceBottomSheet(
|
|||||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.screen_create_room_space_selection_no_space_option),
|
text = stringResource(R.string.screen_create_room_space_selection_no_space_option),
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
@@ -141,7 +141,7 @@ private fun SelectParentSpaceBottomSheet(
|
|||||||
for (space in spaces) {
|
for (space in spaces) {
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = space.displayName,
|
text = space.displayName,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
|
|||||||
@@ -3,14 +3,34 @@
|
|||||||
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
||||||
|
<string name="screen_create_room_error_creating_space">"Ruang tersebut tidak dapat dibuat karena kesalahan yang tidak diketahui. Coba lagi nanti."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Tambahkan nama…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Ruangan baru"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Ruang baru"</string>
|
||||||
<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_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_private_option_title">"Pribadi"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
||||||
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Siapa pun bisa bergabung."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Umum"</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_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_knocking_option_title">"Minta untuk bergabung"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Ajukan permohonan untuk bergabung"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_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_room_access_section_private_option_title">"Pribadi"</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_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_title">"Umum"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Siapa pun di %1$s dapat bergabung."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Standar"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Yang memiliki akses"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(ruang sudah penuh)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_option">"Jangan menambahkan sesuatu ke ruang yang sudah ada."</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Tidak ada ruang yang dipilih"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Tambahkan ke ruang"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Tambahkan deskripsi…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<string name="screen_create_room_room_access_section_public_option_title">"公開"</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_description">"%1$s にいる全員が参加することができます。"</string>
|
||||||
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</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_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_address_section_title">"アドレス"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
|
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
|
||||||
|
|||||||
+29
@@ -20,11 +20,13 @@ import io.element.android.features.createroom.impl.configureroom.CreateRoomConfi
|
|||||||
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
import io.element.android.features.createroom.impl.configureroom.JoinRuleItem
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||||
|
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||||
@@ -56,6 +58,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.matching
|
||||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@@ -517,6 +520,30 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - creating a private room when encryption is disabled by the HS creates it unencrypted`() = runTest {
|
||||||
|
val sessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { true })
|
||||||
|
val createRoomLambda = lambdaRecorder<CreateRoomParameters, Result<RoomId>> { Result.success(A_ROOM_ID) }
|
||||||
|
val matrixClient = createMatrixClient().apply {
|
||||||
|
createRoomResult = createRoomLambda
|
||||||
|
}
|
||||||
|
val presenter = createConfigureRoomPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
sessionEnterpriseService = sessionEnterpriseService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = initialState()
|
||||||
|
|
||||||
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
|
||||||
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
|
|
||||||
|
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
|
||||||
|
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
|
||||||
|
|
||||||
|
createRoomLambda.assertions().isCalledOnce().with(matching<CreateRoomParameters> { !it.isEncrypted })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
@@ -552,6 +579,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
isKnockFeatureEnabled: Boolean = true,
|
isKnockFeatureEnabled: Boolean = true,
|
||||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||||
|
sessionEnterpriseService: FakeSessionEnterpriseService = FakeSessionEnterpriseService(isEncryptionDisabledResult = { false }),
|
||||||
) = ConfigureRoomPresenter(
|
) = ConfigureRoomPresenter(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
initialParentSpaceId = initialParenSpaceId,
|
initialParentSpaceId = initialParenSpaceId,
|
||||||
@@ -566,5 +594,6 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
|
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
|
||||||
),
|
),
|
||||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||||
|
sessionEnterpriseService = sessionEnterpriseService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil deaktivere kontoen din. Denne handlingen kan ikke angres."</string>
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil slette kontoen din. Denne handlingen kan ikke angres."</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages">"Slett alle meldingene mine"</string>
|
<string name="screen_deactivate_account_delete_all_messages">"Slett alle meldingene mine"</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler."</string>
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler."</string>
|
||||||
<string name="screen_deactivate_account_description">"Deaktivering av kontoen din er %1$s , det vil:"</string>
|
<string name="screen_deactivate_account_description">"Hvis %1$s sletter kontoen din, vil følgende skje:"</string>
|
||||||
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
||||||
<string name="screen_deactivate_account_list_item_1">"%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)."</string>
|
<string name="screen_deactivate_account_list_item_1">"%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)."</string>
|
||||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
|
||||||
<string name="screen_deactivate_account_list_item_2">"Fjern deg fra alle chatterom."</string>
|
<string name="screen_deactivate_account_list_item_2">"Fjern deg fra alle chatterom."</string>
|
||||||
<string name="screen_deactivate_account_list_item_3">"Slett kontoinformasjonen din fra vår identitetsserver."</string>
|
<string name="screen_deactivate_account_list_item_3">"Slett kontoinformasjonen din fra vår identitetsserver."</string>
|
||||||
<string name="screen_deactivate_account_list_item_4">"Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem."</string>
|
<string name="screen_deactivate_account_list_item_4">"Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem."</string>
|
||||||
<string name="screen_deactivate_account_title">"Deaktiver kontoen"</string>
|
<string name="screen_deactivate_account_title">"Slett kontoen"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi."</string>
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni oʻchirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib boʻlmaydi."</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages">"Barcha xabarlarimni o‘chirib tashlang"</string>
|
<string name="screen_deactivate_account_delete_all_messages">"Barcha xabarlarimni o‘chirib tashlang"</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."</string>
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."</string>
|
||||||
<string name="screen_deactivate_account_description">"Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"</string>
|
<string name="screen_deactivate_account_description">"Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"</string>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_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">"刪除我所有的訊息"</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages_notice">"警告:未來的使用者可能會看到不完整的對話。"</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">"刪除您的帳號為 %1$s,它將:"</string>
|
||||||
<string name="screen_deactivate_account_description_bold_part">"不可逆"</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">"%1$s 您的帳號(您將無法重新登入,也無法重用您的 ID)。"</string>
|
||||||
<string name="screen_deactivate_account_list_item_1_bold_part">"永久停用"</string>
|
<string name="screen_deactivate_account_list_item_1_bold_part">"永久停用"</string>
|
||||||
<string name="screen_deactivate_account_list_item_2">"將您從所有聊天室移除。"</string>
|
<string name="screen_deactivate_account_list_item_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_list_item_4">"註冊使用者仍可看到您的訊息,但如果您選擇刪除,新使用者與未註冊的使用者將看不到它們。"</string>
|
||||||
<string name="screen_deactivate_account_title">"停用帳號"</string>
|
<string name="screen_deactivate_account_title">"刪除帳號"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ dependencies {
|
|||||||
implementation(projects.libraries.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -10,13 +10,15 @@ package io.element.android.features.enterprise.api
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface EnterpriseService {
|
interface EnterpriseService {
|
||||||
val isEnterpriseBuild: Boolean
|
val isEnterpriseBuild: Boolean
|
||||||
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
||||||
suspend fun tweakMasUrl(url: String, homeserver: String): String
|
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
|
||||||
fun defaultHomeserverList(): List<String>
|
fun defaultHomeserverList(): List<String>
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||||
|
|
||||||
@@ -41,6 +43,16 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
|
||||||
|
*/
|
||||||
|
fun overriddenElementWellKnown(): ElementWellKnown?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
|
||||||
|
*/
|
||||||
|
fun essConfigEndpointUrl(domain: String): String?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -12,5 +12,7 @@ interface SessionEnterpriseService {
|
|||||||
suspend fun isElementCallAvailable(): Boolean
|
suspend fun isElementCallAvailable(): Boolean
|
||||||
suspend fun tweakMasUrl(url: String): String
|
suspend fun tweakMasUrl(url: String): String
|
||||||
|
|
||||||
|
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
||||||
|
|
||||||
suspend fun init()
|
suspend fun init()
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.api.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typealias for the well-known configuration used for enterprise features.
|
||||||
|
*
|
||||||
|
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
|
||||||
|
*/
|
||||||
|
typealias RemoteEnterpriseConfig = ElementWellKnown
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.api.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
|
||||||
|
*/
|
||||||
|
interface RemoteEnterpriseConfigProvider {
|
||||||
|
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ dependencies {
|
|||||||
api(projects.features.enterprise.api)
|
api(projects.features.enterprise.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|||||||
+7
-1
@@ -14,7 +14,9 @@ import dev.zacsweers.metro.ContributesBinding
|
|||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||||||
override val isEnterpriseBuild = false
|
override val isEnterpriseBuild = false
|
||||||
|
|
||||||
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
||||||
override suspend fun tweakMasUrl(url: String, homeserver: String) = url
|
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
|
||||||
override fun defaultHomeserverList(): List<String> = emptyList()
|
override fun defaultHomeserverList(): List<String> = emptyList()
|
||||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||||
|
|
||||||
@@ -45,4 +47,8 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||||
|
|
||||||
|
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
||||||
|
|
||||||
|
override fun essConfigEndpointUrl(domain: String): String? = null
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -17,4 +17,5 @@ class DefaultSessionEnterpriseService : SessionEnterpriseService {
|
|||||||
override suspend fun init() = Unit
|
override suspend fun init() = Unit
|
||||||
override suspend fun tweakMasUrl(url: String): String = url
|
override suspend fun tweakMasUrl(url: String): String = url
|
||||||
override suspend fun isElementCallAvailable(): Boolean = true
|
override suspend fun isElementCallAvailable(): Boolean = true
|
||||||
|
override suspend fun isEncryptionDisabledByHomeserver(): Boolean = false
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.impl.remoteconfig
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultRemoteEnterpriseConfigProvider : RemoteEnterpriseConfigProvider {
|
||||||
|
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
|
||||||
|
return WellknownRetrieverResult.NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class Factory : RemoteEnterpriseConfigProvider.Factory {
|
||||||
|
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider {
|
||||||
|
return DefaultRemoteEnterpriseConfigProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.impl.scanner
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default FOSS implementation of [ContentScannerUrlProvider] that returns `null` for the content scanner URL.
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
|
||||||
|
override suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?> = Result.success(null)
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class Factory : ContentScannerUrlProvider.Factory {
|
||||||
|
override fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider {
|
||||||
|
return DefaultContentScannerUrlProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,6 @@ dependencies {
|
|||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -12,7 +12,9 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -30,7 +32,9 @@ class FakeEnterpriseService(
|
|||||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
||||||
private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() },
|
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
|
||||||
|
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
|
||||||
|
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
|
||||||
) : EnterpriseService {
|
) : EnterpriseService {
|
||||||
private val brandColorState = MutableStateFlow(initialBrandColor)
|
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||||
@@ -39,8 +43,8 @@ class FakeEnterpriseService(
|
|||||||
isEnterpriseUserResult(sessionId)
|
isEnterpriseUserResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun tweakMasUrl(url: String, homeserver: String): String = simulateLongTask {
|
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String = simulateLongTask {
|
||||||
tweakMasUrlResult(url, homeserver)
|
tweakMasUrlResult(url, homeserver, urlContentFetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defaultHomeserverList(): List<String> {
|
override fun defaultHomeserverList(): List<String> {
|
||||||
@@ -79,4 +83,12 @@ class FakeEnterpriseService(
|
|||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
||||||
return getNoisyNotificationChannelIdResult(sessionId)
|
return getNoisyNotificationChannelIdResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
||||||
|
return overrideWellKnownResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun essConfigEndpointUrl(domain: String): String {
|
||||||
|
return essConfigEndpointUrlResult(domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask
|
|||||||
class FakeSessionEnterpriseService(
|
class FakeSessionEnterpriseService(
|
||||||
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||||
private val tweakMasUrlResult: (String) -> String = { lambdaError() },
|
private val tweakMasUrlResult: (String) -> String = { lambdaError() },
|
||||||
|
private val isEncryptionDisabledResult: () -> Boolean = { lambdaError() },
|
||||||
) : SessionEnterpriseService {
|
) : SessionEnterpriseService {
|
||||||
override suspend fun init() {
|
override suspend fun init() {
|
||||||
}
|
}
|
||||||
@@ -26,4 +27,8 @@ class FakeSessionEnterpriseService(
|
|||||||
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||||
isElementCallAvailableResult()
|
isElementCallAvailableResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isEncryptionDisabledByHomeserver(): Boolean {
|
||||||
|
return isEncryptionDisabledResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.test.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
|
||||||
|
class FakeRemoteEnterpriseConfigProvider(
|
||||||
|
private val getResult: (SessionId) -> WellknownRetrieverResult<RemoteEnterpriseConfig> = { WellknownRetrieverResult.NotFound }
|
||||||
|
) : RemoteEnterpriseConfigProvider {
|
||||||
|
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
|
||||||
|
return getResult(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* 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.enterprise.test.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
|
|
||||||
|
class FakeRemoteEnterpriseConfigProviderFactory(
|
||||||
|
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||||
|
) : RemoteEnterpriseConfigProvider.Factory {
|
||||||
|
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_identity_confirmation_cannot_confirm">"نمیتوانید تأیید کنید؟"</string>
|
<string name="screen_identity_confirmation_cannot_confirm">"نمیتوانید تأیید کنید؟"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"تأیید این افزاره برای برپایی پیامرسانی امن."</string>
|
<string name="screen_identity_confirmation_subtitle">"گزینش چگونگی تأیید برای برپایی پیامرسانی امن."</string>
|
||||||
<string name="screen_identity_confirmation_title">"تأیید هویتتان"</string>
|
<string name="screen_identity_confirmation_title">"تأیید هویت دیجیتالیتان"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزارهای دیگر"</string>
|
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزارهای دیگر"</string>
|
||||||
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
|
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
|
||||||
<string name="screen_identity_confirmed_subtitle">"اکنون میتوانید پیامها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ میزنید نیز میتواند به این افزاره اعتماد کند."</string>
|
<string name="screen_identity_confirmed_subtitle">"اکنون میتوانید پیامها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ میزنید نیز میتواند به این افزاره اعتماد کند."</string>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
|
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"Verifiser denne enheten for å sette opp sikker meldingsutveksling."</string>
|
<string name="screen_identity_confirmation_subtitle">"Velg hvordan du vil verifisere deg for å sette opp sikker meldingsutveksling."</string>
|
||||||
<string name="screen_identity_confirmation_title">"Bekreft din digitale identitet"</string>
|
<string name="screen_identity_confirmation_title">"Bekreft din digitale identitet"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
|
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
|
||||||
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
|
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
|
||||||
|
|||||||
+23
-14
@@ -14,11 +14,15 @@ import androidx.activity.compose.BackHandler
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -65,6 +69,8 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalFloa
|
|||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
|
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -201,6 +207,9 @@ private fun HomeScaffold(
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
HomeBottomBar(
|
HomeBottomBar(
|
||||||
|
// The Scaffold uses top-only insets so the scrollable content can go edge-to-edge behind the
|
||||||
|
// navigation bar, so the floating toolbar has to apply the bottom inset itself to avoid overlapping it.
|
||||||
|
modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
// scroll to top if selecting the same item
|
// scroll to top if selecting the same item
|
||||||
@@ -234,7 +243,15 @@ private fun HomeScaffold(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.Center,
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
|
val outerPadding = PaddingValues(
|
||||||
|
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
||||||
|
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
|
// Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
|
||||||
|
bottom = padding.calculateBottomPadding(),
|
||||||
|
top = padding.calculateTopPadding()
|
||||||
|
)
|
||||||
val contentPadding = PaddingValues(
|
val contentPadding = PaddingValues(
|
||||||
bottom = 96.dp,
|
bottom = 96.dp,
|
||||||
)
|
)
|
||||||
@@ -251,18 +268,10 @@ private fun HomeScaffold(
|
|||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
onRoomClick = ::onRoomClick,
|
onRoomClick = ::onRoomClick,
|
||||||
onCreateRoomClick = onStartChatClick,
|
onCreateRoomClick = onStartChatClick,
|
||||||
contentPadding = contentPadding,
|
contentPadding = lazyColumnContentPadding + contentPadding,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(outerPadding)
|
||||||
PaddingValues(
|
.consumeWindowInsets(outerPadding)
|
||||||
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
|
||||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
|
||||||
// Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
|
|
||||||
bottom = padding.calculateBottomPadding(),
|
|
||||||
top = padding.calculateTopPadding()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
)
|
)
|
||||||
SpaceFiltersView(roomListState.spaceFiltersState)
|
SpaceFiltersView(roomListState.spaceFiltersState)
|
||||||
@@ -271,10 +280,10 @@ private fun HomeScaffold(
|
|||||||
HomeSpacesView(
|
HomeSpacesView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(outerPadding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(outerPadding)
|
||||||
.hazeSource(state = hazeState),
|
.hazeSource(state = hazeState),
|
||||||
contentPadding = contentPadding,
|
contentPadding = lazyColumnContentPadding + contentPadding,
|
||||||
state = state.homeSpacesState,
|
state = state.homeSpacesState,
|
||||||
lazyListState = spacesLazyListState,
|
lazyListState = spacesLazyListState,
|
||||||
onSpaceClick = { spaceId ->
|
onSpaceClick = { spaceId ->
|
||||||
|
|||||||
+135
-7
@@ -8,12 +8,20 @@
|
|||||||
|
|
||||||
package io.element.android.features.home.impl.components
|
package io.element.android.features.home.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.pager.VerticalPager
|
import androidx.compose.foundation.pager.VerticalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -21,8 +29,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -33,10 +44,20 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.heading
|
import androidx.compose.ui.semantics.heading
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.appconfig.RoomListConfig
|
import io.element.android.appconfig.RoomListConfig
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
@@ -67,10 +88,13 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toEmojiText
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -96,13 +120,15 @@ fun HomeTopBar(
|
|||||||
spaceFiltersState: SpaceFiltersState,
|
spaceFiltersState: SpaceFiltersState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.backgroundVerticalGradient(
|
.backgroundVerticalGradient(
|
||||||
isVisible = !areSearchResultsDisplayed,
|
isVisible = !areSearchResultsDisplayed,
|
||||||
)
|
)
|
||||||
.statusBarsPadding(),
|
.statusBarsPadding()
|
||||||
|
.padding(contentPadding),
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
scrolledContainerColor = Color.Transparent,
|
scrolledContainerColor = Color.Transparent,
|
||||||
@@ -145,7 +171,7 @@ fun HomeTopBar(
|
|||||||
},
|
},
|
||||||
// We want a 16dp left padding for the navigationIcon :
|
// We want a 16dp left padding for the navigationIcon :
|
||||||
// 4dp from default TopAppBarHorizontalPadding
|
// 4dp from default TopAppBarHorizontalPadding
|
||||||
// 8dp from AccountIcon default padding (because of IconButton)
|
// 8dp from AccountIcon default padding (48dp touch target around 32dp avatar)
|
||||||
// 4dp extra padding using left insets
|
// 4dp extra padding using left insets
|
||||||
windowInsets = WindowInsets(left = 4.dp),
|
windowInsets = WindowInsets(left = 4.dp),
|
||||||
)
|
)
|
||||||
@@ -153,7 +179,7 @@ fun HomeTopBar(
|
|||||||
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
|
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
|
||||||
RoomListFiltersView(
|
RoomListFiltersView(
|
||||||
state = filtersState,
|
state = filtersState,
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp).padding(contentPadding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,19 +336,36 @@ private fun AccountIcon(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
|
val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
|
||||||
IconButton(
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
modifier = modifier.then(testTag),
|
Box(
|
||||||
onClick = onClick,
|
modifier = modifier
|
||||||
|
.then(testTag)
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
indication = ripple(bounded = false),
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Box {
|
|
||||||
val avatarData by remember(matrixUser) {
|
val avatarData by remember(matrixUser) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
|
||||||
|
val avatarModifier = if (statusEmoji != null) {
|
||||||
|
Modifier.eraseStatusEmojiBackground(
|
||||||
|
parentSize = AvatarSize.CurrentUserTopBar.dp,
|
||||||
|
layoutDirection = LocalLayoutDirection.current,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
|
modifier = avatarModifier,
|
||||||
contentDescription = if (isCurrentAccount) {
|
contentDescription = if (isCurrentAccount) {
|
||||||
if (showAvatarIndicator) {
|
if (showAvatarIndicator) {
|
||||||
stringResource(CommonStrings.a11y_settings_with_required_action)
|
stringResource(CommonStrings.a11y_settings_with_required_action)
|
||||||
@@ -333,11 +376,69 @@ private fun AccountIcon(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (statusEmoji != null) {
|
||||||
|
StatusEmojiBadge(
|
||||||
|
emoji = statusEmoji,
|
||||||
|
modifier = Modifier.align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (showAvatarIndicator) {
|
if (showAvatarIndicator) {
|
||||||
RedIndicatorAtom(
|
RedIndicatorAtom(
|
||||||
modifier = Modifier.align(Alignment.TopEnd)
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val statusEmojiBadgeSize = 20.dp
|
||||||
|
private val statusEmojiBadgeRadius = statusEmojiBadgeSize / 2
|
||||||
|
private val statusEmojiBadgeOffset = 8.dp
|
||||||
|
|
||||||
|
private fun Modifier.eraseStatusEmojiBackground(
|
||||||
|
parentSize: Dp,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
): Modifier = this
|
||||||
|
.graphicsLayer {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawCircle(
|
||||||
|
color = Color.Black,
|
||||||
|
center = Offset(
|
||||||
|
x = if (layoutDirection == LayoutDirection.Ltr) {
|
||||||
|
(parentSize - statusEmojiBadgeRadius + statusEmojiBadgeOffset).toPx()
|
||||||
|
} else {
|
||||||
|
(statusEmojiBadgeRadius - statusEmojiBadgeOffset).toPx()
|
||||||
|
},
|
||||||
|
y = size.height - statusEmojiBadgeRadius.toPx(),
|
||||||
|
),
|
||||||
|
radius = statusEmojiBadgeRadius.toPx(),
|
||||||
|
blendMode = BlendMode.Clear,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusEmojiBadge(
|
||||||
|
emoji: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
// Ensure the emoji size is not affected by the user's font scale settings
|
||||||
|
val fixedFontScaleDensity = remember(density.density) {
|
||||||
|
Density(density = density.density, fontScale = 1f)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.offset(x = statusEmojiBadgeOffset)
|
||||||
|
.size(statusEmojiBadgeSize),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) {
|
||||||
|
Text(
|
||||||
|
text = emoji,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,6 +527,33 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun HomeTopBarWithStatusPreview() = ElementPreview {
|
||||||
|
HomeTopBar(
|
||||||
|
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||||
|
currentUserAndNeighbors = persistentListOf(
|
||||||
|
aMatrixUser(
|
||||||
|
id = "@id:domain",
|
||||||
|
displayName = USER_NAME_ALICE,
|
||||||
|
displayedStatus = DisplayedStatus.UserSet(UserStatus(emoji = "🌴", text = "Away")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
showAvatarIndicator = false,
|
||||||
|
areSearchResultsDisplayed = false,
|
||||||
|
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||||
|
onOpenSettings = {},
|
||||||
|
onAccountSwitch = {},
|
||||||
|
onToggleSearch = {},
|
||||||
|
canReportBug = true,
|
||||||
|
displayFilters = true,
|
||||||
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
|
onMenuActionClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+14
-11
@@ -63,6 +63,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
|
|||||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -126,7 +128,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = room.timestamp,
|
timestamp = room.timestamp,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = room.dmUserStatus,
|
||||||
)
|
)
|
||||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = null,
|
timestamp = null,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = null,
|
||||||
)
|
)
|
||||||
if (room.canonicalAlias != null) {
|
if (room.canonicalAlias != null) {
|
||||||
Text(
|
Text(
|
||||||
@@ -218,22 +222,21 @@ private fun NameAndTimestampRow(
|
|||||||
name: String?,
|
name: String?,
|
||||||
timestamp: String?,
|
timestamp: String?,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
|
dmUserStatus: DisplayedStatus?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = spacedBy(16.dp)
|
horizontalArrangement = spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
|
||||||
modifier = Modifier
|
DisplayNameWithStatus(
|
||||||
.weight(1f)
|
name = displayName,
|
||||||
.clipToBounds(),
|
status = dmUserStatus,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
|
nameColor = ElementTheme.colors.roomListRoomName,
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
nameFontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
color = ElementTheme.colors.roomListRoomName,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+2
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.CallIntentConsensus
|
|||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
|
|||||||
}.toImmutableList(),
|
}.toImmutableList(),
|
||||||
isTombstoned = roomInfo.successorRoom != null,
|
isTombstoned = roomInfo.successorRoom != null,
|
||||||
isSpace = roomInfo.isSpace,
|
isSpace = roomInfo.isSpace,
|
||||||
|
dmUserStatus = roomInfo.dmUserStatus(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -22,6 +22,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
|
|||||||
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
||||||
|
|
||||||
override fun select(filter: RoomListFilter) {
|
override fun select(filter: RoomListFilter) {
|
||||||
|
if (selectedFilters.any { it in filter.incompatibleFilters }) return
|
||||||
selectedFilters.add(filter)
|
selectedFilters.add(filter)
|
||||||
filterSelectionStates.value = buildFilters()
|
filterSelectionStates.value = buildFilters()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
|
|||||||
val isTombstoned: Boolean,
|
val isTombstoned: Boolean,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
|
val dmUserStatus: DisplayedStatus?,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
|
|||||||
+4
-1
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
|
|||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
|
|||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
isSpace = isSpace
|
isSpace = isSpace,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-7
@@ -94,7 +94,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = contextMenu.roomName ?: stringResource(id = CommonStrings.common_no_room_name),
|
text = contextMenu.roomName ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
@@ -104,7 +104,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
)
|
)
|
||||||
if (contextMenu.hasNewContent) {
|
if (contextMenu.hasNewContent) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.action_mark_as_read),
|
text = stringResource(id = CommonStrings.action_mark_as_read),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@@ -117,7 +117,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
|
text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@@ -135,7 +135,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
CommonStrings.common_favourite to CompoundIcons.Favourite()
|
CommonStrings.common_favourite to CompoundIcons.Favourite()
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = textResId),
|
text = stringResource(id = textResId),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@@ -154,7 +154,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.common_settings),
|
text = stringResource(id = CommonStrings.common_settings),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@@ -169,7 +169,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
)
|
)
|
||||||
if (canReportRoom) {
|
if (canReportRoom) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(text = stringResource(CommonStrings.action_report_room))
|
Text(text = stringResource(CommonStrings.action_report_room))
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable { onReportRoomClick() },
|
modifier = Modifier.clickable { onReportRoomClick() },
|
||||||
@@ -182,7 +182,7 @@ private fun RoomListModalBottomSheetContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(text = stringResource(CommonStrings.action_leave_room))
|
Text(text = stringResource(CommonStrings.action_leave_room))
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable { onLeaveRoomClick() },
|
modifier = Modifier.clickable { onLeaveRoomClick() },
|
||||||
|
|||||||
+9
-1
@@ -13,9 +13,15 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -153,7 +159,8 @@ private fun RoomListSearchContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom),
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -167,6 +174,7 @@ private fun RoomListSearchContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = state.results,
|
items = state.results,
|
||||||
|
|||||||
+8
-7
@@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -45,6 +43,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchField
|
import io.element.android.libraries.designsystem.theme.components.SearchField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -75,9 +75,7 @@ fun SpaceFiltersView(
|
|||||||
}
|
}
|
||||||
if (sheetState.isVisible || isSelecting) {
|
if (sheetState.isVisible || isSelecting) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.systemBarsPadding()
|
|
||||||
.navigationBarsPadding(),
|
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
if (state is SpaceFiltersState.Selecting) {
|
if (state is SpaceFiltersState.Selecting) {
|
||||||
@@ -85,6 +83,7 @@ fun SpaceFiltersView(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
|
contentWindowInsets = { scaffoldScrollableContentInsets },
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -113,7 +112,7 @@ private fun SpaceFiltersBottomSheetContent(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(vertical = 16.dp)
|
modifier = modifier.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.screen_roomlist_your_spaces),
|
text = stringResource(R.string.screen_roomlist_your_spaces),
|
||||||
@@ -131,7 +130,9 @@ private fun SpaceFiltersBottomSheetContent(
|
|||||||
placeholder = stringResource(CommonStrings.action_search),
|
placeholder = stringResource(CommonStrings.action_search),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
LazyColumn {
|
LazyColumn(
|
||||||
|
contentPadding = lazyColumnContentPadding,
|
||||||
|
) {
|
||||||
items(filters) { filter ->
|
items(filters) { filter ->
|
||||||
SpaceFilterItem(
|
SpaceFilterItem(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
|
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
|
||||||
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
|
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
|
||||||
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
|
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
|
||||||
|
<string name="banner_new_sound_message">"Pemberitahuan Anda telah diperbarui—lebih jelas, lebih cepat, dan tidak mengganggu."</string>
|
||||||
|
<string name="banner_new_sound_title">"Kami telah memperbarui suara Anda."</string>
|
||||||
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Siapkan Kunci pemulihan"</string>
|
<string name="banner_set_up_recovery_submit">"Siapkan Kunci pemulihan"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Cadangkan obrolan Anda"</string>
|
<string name="banner_set_up_recovery_title">"Cadangkan obrolan Anda"</string>
|
||||||
@@ -48,6 +50,7 @@ Anda tidak memiliki pesan yang belum dibaca!"</string>
|
|||||||
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
||||||
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
||||||
|
<string name="screen_roomlist_your_spaces">"Ruang Anda"</string>
|
||||||
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
||||||
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
|
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
|
||||||
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."</string>
|
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."</string>
|
||||||
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
|
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
|
||||||
<string name="banner_set_up_recovery_content">"Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter."</string>
|
<string name="banner_set_up_recovery_content">"Chattene dine sikkerhetskopieres automatisk med ende-til-ende-kryptering. For å gjenopprette denne sikkerhetskopien og beholde din digitale identitet når du mister tilgang til alle enhetene dine, trenger du gjenopprettingsnøkkelen din."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Hent gjenopprettingsnøkkel"</string>
|
<string name="banner_set_up_recovery_submit">"Hent gjenopprettingsnøkkel"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Konfigurer gjenoppretting for å beskytte kontoen din"</string>
|
<string name="banner_set_up_recovery_title">"Sikkerhetskopier chattene dine"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
|
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
|
||||||
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
|
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
|
||||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
|
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<string name="banner_battery_optimization_title_android">"Powiadomienia nie dochodzą?"</string>
|
<string name="banner_battery_optimization_title_android">"Powiadomienia nie dochodzą?"</string>
|
||||||
<string name="banner_new_sound_message">"Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy."</string>
|
<string name="banner_new_sound_message">"Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy."</string>
|
||||||
<string name="banner_new_sound_title">"Odświeżyliśmy Twoje dźwięki"</string>
|
<string name="banner_new_sound_title">"Odświeżyliśmy Twoje dźwięki"</string>
|
||||||
<string name="banner_set_up_recovery_content">"Twoje czaty są automatycznie archiwizowane za pomocą szyfrowania end-to-end. Aby przywrócić tę kopię zapasową i swoją tożsamość cyfrową, wymagany będzie klucz przywracania."</string>
|
<string name="banner_set_up_recovery_content">"Twoje czaty są automatycznie archiwizowane za pomocą szyfrowania end-to-end. Aby przywrócić kopię zapasową i swoją tożsamość cyfrową, wymagany będzie klucz przywracania."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Uzyskaj klucz przywracania"</string>
|
<string name="banner_set_up_recovery_submit">"Uzyskaj klucz przywracania"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Utwórz kopię zapasową swoich czatów"</string>
|
<string name="banner_set_up_recovery_title">"Utwórz kopię zapasową swoich czatów"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Potwierdź klucz przywracania, aby zachować dostęp do magazynu kluczy i historii wiadomości."</string>
|
<string name="confirm_recovery_key_banner_message">"Potwierdź klucz przywracania, aby zachować dostęp do magazynu kluczy i historii wiadomości."</string>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<string name="banner_battery_optimization_title_android">"Nu primiți notificări?"</string>
|
<string name="banner_battery_optimization_title_android">"Nu primiți notificări?"</string>
|
||||||
<string name="banner_new_sound_message">"Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar."</string>
|
<string name="banner_new_sound_message">"Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar."</string>
|
||||||
<string name="banner_new_sound_title">"Am reîmprospătat sunetele"</string>
|
<string name="banner_new_sound_title">"Am reîmprospătat sunetele"</string>
|
||||||
<string name="banner_set_up_recovery_content">"Chaturile dumneavoastră sunt salvate automat cu criptare end-to-end. Pentru a restaura această copie de rezervă și a vă păstra identitatea digitală atunci când pierzdeți accesul la toate dispozitivele dumneavoastră, veți avea nevoie de cheia de recuperare."</string>
|
<string name="banner_set_up_recovery_content">"Chaturile dumneavoastră sunt salvate automat cu criptare end-to-end. Pentru a restaura aceast backup și a vă păstra identitatea digitală atunci când pierzdeți accesul la toate dispozitivele dumneavoastră, veți avea nevoie de cheia de recuperare."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Obțineți cheia de recuperare"</string>
|
<string name="banner_set_up_recovery_submit">"Obțineți cheia de recuperare"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Faceți un backup al mesajelor"</string>
|
<string name="banner_set_up_recovery_title">"Faceți un backup al mesajelor"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."</string>
|
<string name="confirm_recovery_key_banner_message">"Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."</string>
|
||||||
|
|||||||
+43
@@ -72,6 +72,49 @@ class RoomListFiltersPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun `present - toggling two incompatible filters in the same frame keeps the first one`() = runTest {
|
||||||
|
val presenter = createRoomListFiltersPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Favourites))
|
||||||
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Invites))
|
||||||
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
|
awaitLastSequentialItem().let { state ->
|
||||||
|
assertThat(state.selectedFilters()).containsExactly(RoomListFilter.Favourites)
|
||||||
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
filterSelectionState(RoomListFilter.Favourites, true),
|
||||||
|
filterSelectionState(RoomListFilter.Unread, false),
|
||||||
|
filterSelectionState(RoomListFilter.People, false),
|
||||||
|
filterSelectionState(RoomListFilter.Rooms, false),
|
||||||
|
).inOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun `present - toggling two incompatible categories in the same frame keeps the first one`() = runTest {
|
||||||
|
val presenter = createRoomListFiltersPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.People))
|
||||||
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
|
awaitLastSequentialItem().let { state ->
|
||||||
|
assertThat(state.selectedFilters()).containsExactly(RoomListFilter.People)
|
||||||
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
filterSelectionState(RoomListFilter.People, true),
|
||||||
|
filterSelectionState(RoomListFilter.Unread, false),
|
||||||
|
filterSelectionState(RoomListFilter.Favourites, false),
|
||||||
|
).inOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - clear filters event`() = runTest {
|
fun `present - clear filters event`() = runTest {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user