mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 04:15:12 +00:00
Merge develop into feature/account-provider-selection
This commit is contained in:
@@ -51,12 +51,12 @@ jobs:
|
|||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APKs
|
- name: Assemble debug APKs
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug Gplay Enterprise APK
|
- name: Assemble debug Gplay Enterprise APK
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ jobs:
|
|||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ jobs:
|
|||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ jobs:
|
|||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
|
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: false
|
cache-read-only: false
|
||||||
|
|
||||||
@@ -80,12 +80,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|||||||
@@ -69,12 +69,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
@@ -108,12 +108,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Konsist tests
|
- name: Run Konsist tests
|
||||||
@@ -149,12 +149,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run compose tests
|
- name: Run compose tests
|
||||||
@@ -183,12 +183,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build Gplay Debug
|
- name: Build Gplay Debug
|
||||||
@@ -228,12 +228,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Detekt
|
- name: Run Detekt
|
||||||
@@ -269,12 +269,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run Ktlint check
|
- name: Run Ktlint check
|
||||||
@@ -336,7 +336,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
- uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||||
|
|
||||||
upload_reports:
|
upload_reports:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
@@ -82,12 +82,12 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create Enterprise app bundle
|
- name: Create Enterprise app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
@@ -126,12 +126,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
- name: Create APKs
|
- name: Create APKs
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ else
|
|||||||
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
||||||
fi
|
fi
|
||||||
git add -A
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Screenshots are already up to date, nothing to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
git commit -m "Update screenshots"
|
git commit -m "Update screenshots"
|
||||||
|
|
||||||
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ jobs:
|
|||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build debug code and test fixtures
|
- name: Build debug code and test fixtures
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|||||||
+92
@@ -1,3 +1,95 @@
|
|||||||
|
Changes in Element X v26.08.1
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
|
||||||
|
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
|
||||||
|
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
|
||||||
|
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
|
||||||
|
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
|
||||||
|
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
|
||||||
|
### 🧱 Build
|
||||||
|
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
|
||||||
|
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
|
||||||
|
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
|
||||||
|
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
|
||||||
|
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
|
||||||
|
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
|
||||||
|
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
|
||||||
|
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
|
||||||
|
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
|
||||||
|
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
|
||||||
|
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
|
||||||
|
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
|
||||||
|
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
|
||||||
|
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
|
||||||
|
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
|
||||||
|
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
|
||||||
|
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
|
||||||
|
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
|
||||||
|
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
|
||||||
|
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
|
||||||
|
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
|
||||||
|
### 💼 Element Pro
|
||||||
|
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
|
||||||
|
### Others
|
||||||
|
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
|
||||||
|
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
|
||||||
|
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
|
||||||
|
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
|
||||||
|
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
|
||||||
|
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
|
||||||
|
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
|
||||||
|
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
|
||||||
|
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
|
||||||
|
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
|
||||||
|
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
|
||||||
|
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
|
||||||
|
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
|
||||||
|
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
|
||||||
|
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
|
||||||
|
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
|
||||||
|
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
|
||||||
|
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
|
||||||
|
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
|
||||||
|
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
|
||||||
|
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
|
||||||
|
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
|
||||||
|
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
|
||||||
|
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
|
||||||
|
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
|
||||||
|
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
|
||||||
|
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
|
||||||
|
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
|
||||||
|
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
|
||||||
|
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
|
||||||
|
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
|
||||||
|
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
|
||||||
|
|
||||||
Changes in Element X v26.08.0
|
Changes in Element X v26.08.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.libraries.uiCommon)
|
implementation(projects.libraries.uiCommon)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(projects.features.login.api)
|
implementation(projects.features.login.api)
|
||||||
|
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
@@ -60,6 +61,7 @@ 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.permissions.test)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ package io.element.android.appnav
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -49,11 +51,13 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.home.api.HomeEntryPoint
|
import io.element.android.features.home.api.HomeEntryPoint
|
||||||
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
||||||
|
import io.element.android.features.location.api.LocalMapTilerConfig
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
@@ -153,6 +157,7 @@ class LoggedInFlowNode(
|
|||||||
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
|
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Placeholder,
|
initialElement = NavTarget.Placeholder,
|
||||||
@@ -677,6 +682,12 @@ class LoggedInFlowNode(
|
|||||||
val colors by remember {
|
val colors by remember {
|
||||||
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
|
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
|
||||||
}.collectAsState(SemanticColorsLightDark.default)
|
}.collectAsState(SemanticColorsLightDark.default)
|
||||||
|
|
||||||
|
val currentMapTilerConfig = LocalMapTilerConfig.current
|
||||||
|
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
|
||||||
|
value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig
|
||||||
|
}
|
||||||
|
|
||||||
ElementThemeApp(
|
ElementThemeApp(
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
@@ -684,16 +695,18 @@ class LoggedInFlowNode(
|
|||||||
compoundDark = colors.dark,
|
compoundDark = colors.dark,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
) {
|
) {
|
||||||
val isOnline by syncService.isOnline.collectAsState()
|
CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) {
|
||||||
ConnectivityIndicatorContainer(
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
isOnline = isOnline,
|
ConnectivityIndicatorContainer(
|
||||||
modifier = modifier,
|
isOnline = isOnline,
|
||||||
) { contentModifier ->
|
modifier = modifier,
|
||||||
Box(modifier = contentModifier) {
|
) { contentModifier ->
|
||||||
val ftueState by ftueService.state.collectAsState()
|
Box(modifier = contentModifier) {
|
||||||
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
|
val ftueState by ftueService.state.collectAsState()
|
||||||
if (ftueState is FtueState.Complete) {
|
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
if (ftueState is FtueState.Complete) {
|
||||||
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,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
|
||||||
@@ -65,6 +67,7 @@ class LoggedInPresenter(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||||
@@ -120,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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -48,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
|
||||||
@@ -127,6 +131,7 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
).test {
|
).test {
|
||||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
@@ -346,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()
|
||||||
@@ -362,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,
|
||||||
@@ -373,6 +430,7 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.sonarqube.gradle.SonarResolverTask
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2022-2025 New Vector Ltd.
|
* Copyright 2022-2025 New Vector Ltd.
|
||||||
@@ -18,3 +20,9 @@ dependencies {
|
|||||||
implementation(libs.ksp.plugin)
|
implementation(libs.ksp.plugin)
|
||||||
implementation(libs.kotlinpoet.ksp)
|
implementation(libs.kotlinpoet.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the processResources task is executed before the SonarResolverTask to avoid issues with missing resources during SonarQube analysis.
|
||||||
|
// This *should not be necesssary* but is a workaround for a known issue with the SonarQube Gradle plugin.
|
||||||
|
tasks.withType<SonarResolverTask>().configureEach {
|
||||||
|
dependsOn("processResources")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: 9b1ee4cd88...f5f910856c
@@ -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 = {
|
||||||
|
|||||||
+19
@@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues the full-screen announcements shown to the user once, such as the notice about a new feature.
|
||||||
|
*
|
||||||
|
* A dismissed announcement is remembered so it is not shown again.
|
||||||
|
*/
|
||||||
interface AnnouncementService {
|
interface AnnouncementService {
|
||||||
|
/**
|
||||||
|
* Queues an announcement, unless the user has already dismissed it.
|
||||||
|
*
|
||||||
|
* @param announcement the announcement to show.
|
||||||
|
*/
|
||||||
suspend fun showAnnouncement(announcement: Announcement)
|
suspend fun showAnnouncement(announcement: Announcement)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that the user dismissed an announcement, so it is not queued again.
|
||||||
|
*
|
||||||
|
* @param announcement the announcement that was dismissed.
|
||||||
|
*/
|
||||||
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
||||||
|
|
||||||
|
/** The announcements still waiting to be shown, in the order they should appear. */
|
||||||
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this composable to render the announcement UI in Fullscreen.
|
* Use this composable to render the announcement UI in Fullscreen.
|
||||||
|
* Draws nothing while there is no announcement to show.
|
||||||
|
*
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
|
|||||||
+3
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
package io.element.android.features.cachecleaner.api
|
package io.element.android.features.cachecleaner.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the temporary files holding decrypted content, so plaintext media does not linger on disk.
|
||||||
|
*/
|
||||||
interface CacheCleaner {
|
interface CacheCleaner {
|
||||||
/**
|
/**
|
||||||
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
|
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
|
||||||
|
|||||||
+3
@@ -10,6 +10,9 @@ package io.element.android.features.call.api
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it.
|
||||||
|
*/
|
||||||
interface CurrentCallService {
|
interface CurrentCallService {
|
||||||
/**
|
/**
|
||||||
* The current call state flow, which will be updated when the active call changes.
|
* The current call state flow, which will be updated when the active call changes.
|
||||||
|
|||||||
+6
@@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
|
|||||||
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
||||||
*/
|
*/
|
||||||
fun interface ContentScannerService {
|
fun interface ContentScannerService {
|
||||||
|
/**
|
||||||
|
* Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning.
|
||||||
|
*
|
||||||
|
* @param mediaSources the media to scan.
|
||||||
|
* @param contentValidationState where the outcome of each scan is published for the UI to observe.
|
||||||
|
*/
|
||||||
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
/**
|
||||||
|
* Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens.
|
||||||
|
*/
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
/**
|
||||||
|
* @param isSpace true to create a space rather than a room.
|
||||||
|
*/
|
||||||
fun setIsSpace(isSpace: Boolean): Builder
|
fun setIsSpace(isSpace: Boolean): Builder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param parentSpaceId the space the new room should be added to once created.
|
||||||
|
*/
|
||||||
fun setParentSpace(parentSpaceId: RoomId): Builder
|
fun setParentSpace(parentSpaceId: RoomId): Builder
|
||||||
|
|
||||||
|
/** Builds the node with the options configured so far. */
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+61
-1
@@ -10,15 +10,46 @@ 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 io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the restrictions and customisations an enterprise deployment can impose: allowed homeservers, branding, push gateways and more.
|
||||||
|
*
|
||||||
|
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
|
||||||
|
*/
|
||||||
interface EnterpriseService {
|
interface EnterpriseService {
|
||||||
|
/** Whether this build is an enterprise one; `false` for the public app. */
|
||||||
val isEnterpriseBuild: Boolean
|
val isEnterpriseBuild: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
|
||||||
|
*
|
||||||
|
* @param sessionId the session to check.
|
||||||
|
*/
|
||||||
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
||||||
suspend fun tweakMasUrl(url: String, homeserver: String): String
|
|
||||||
|
/**
|
||||||
|
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
|
||||||
|
*
|
||||||
|
* @param url the URL to rewrite.
|
||||||
|
* @param homeserver the homeserver the URL belongs to.
|
||||||
|
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
|
||||||
|
*/
|
||||||
|
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
|
||||||
|
*/
|
||||||
fun defaultHomeserverList(): List<String>
|
fun defaultHomeserverList(): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
|
||||||
|
*
|
||||||
|
* @param homeserverUrl the server the user is trying to use.
|
||||||
|
*/
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,17 +59,38 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
|
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The brand color to apply, or `null` when the default theme color should be used.
|
||||||
|
*
|
||||||
|
* @param sessionId the session to read the color of, or `null` for the color used when no session is active.
|
||||||
|
*/
|
||||||
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
|
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full light and dark color palettes derived from the brand color.
|
||||||
|
*
|
||||||
|
* @param sessionId the session to read the colors of, or `null` for the colors used when no session is active.
|
||||||
|
*/
|
||||||
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
|
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
|
||||||
|
|
||||||
|
/** The push gateway to register with when using Firebase, or `null` to use the app default. */
|
||||||
fun firebasePushGateway(): String?
|
fun firebasePushGateway(): String?
|
||||||
|
|
||||||
|
/** The push gateway to register with when using UnifiedPush, or `null` to use the app default. */
|
||||||
fun unifiedPushDefaultPushGateway(): String?
|
fun unifiedPushDefaultPushGateway(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where bug reports should be submitted, which an enterprise deployment can redirect or disable entirely.
|
||||||
|
*
|
||||||
|
* @param sessionId the session to read the setting of, or `null` for the setting used when no session is active.
|
||||||
|
*/
|
||||||
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
|
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets Notification Channel to use for the noisy notifications of the provided session.
|
* Gets Notification Channel to use for the noisy notifications of the provided session.
|
||||||
|
* Returns `null` when the session has no dedicated channel and the app-wide one should be used.
|
||||||
|
*
|
||||||
|
* @param sessionId the session whose channel is requested.
|
||||||
*/
|
*/
|
||||||
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
||||||
|
|
||||||
@@ -47,6 +99,14 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
fun overriddenElementWellKnown(): ElementWellKnown?
|
fun overriddenElementWellKnown(): ElementWellKnown?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
|
||||||
|
* Returns `null` when this build does not read its configuration from an ESS deployment.
|
||||||
|
*
|
||||||
|
* @param domain the server whose configuration endpoint is requested.
|
||||||
|
*/
|
||||||
|
fun essConfigEndpointUrl(domain: String): String?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -8,11 +8,25 @@
|
|||||||
|
|
||||||
package io.element.android.features.enterprise.api
|
package io.element.android.features.enterprise.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The session-scoped counterpart of [EnterpriseService], answering the same kind of questions for the session it belongs to.
|
||||||
|
*
|
||||||
|
* [init] must have run before the other members return meaningful answers.
|
||||||
|
*/
|
||||||
interface SessionEnterpriseService {
|
interface SessionEnterpriseService {
|
||||||
|
/** Whether Element Call may be used on this session, which a deployment can turn off. */
|
||||||
suspend fun isElementCallAvailable(): Boolean
|
suspend fun isElementCallAvailable(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrites the authentication server URL for this session; see [EnterpriseService.tweakMasUrl].
|
||||||
|
*
|
||||||
|
* @param url the URL to rewrite.
|
||||||
|
*/
|
||||||
suspend fun tweakMasUrl(url: String): String
|
suspend fun tweakMasUrl(url: String): String
|
||||||
|
|
||||||
|
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
|
||||||
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
||||||
|
|
||||||
|
/** Loads the deployment configuration of this session; to be called once when the session starts. */
|
||||||
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
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
/**
|
||||||
|
* Returns the configuration of a session, served from the cache when possible.
|
||||||
|
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
|
||||||
|
*
|
||||||
|
* @param sessionId the session whose configuration is requested.
|
||||||
|
*/
|
||||||
|
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
|
||||||
|
*/
|
||||||
|
interface Factory {
|
||||||
|
/**
|
||||||
|
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
|
||||||
|
*/
|
||||||
|
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -14,6 +14,7 @@ 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 io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -24,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
|
||||||
|
|
||||||
@@ -48,4 +49,6 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||||
|
|
||||||
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
||||||
|
|
||||||
|
override fun essConfigEndpointUrl(domain: String): String? = null
|
||||||
}
|
}
|
||||||
|
|||||||
+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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-2
@@ -5,10 +5,12 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.scanner
|
package io.element.android.features.enterprise.impl.scanner
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
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
|
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,5 +18,12 @@ import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
|||||||
*/
|
*/
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
|
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
|
||||||
override suspend fun getContentScannerUrl(homeserver: String): Result<String?> = Result.success(null)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+9
-3
@@ -12,6 +12,7 @@ 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.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
@@ -31,8 +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 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)
|
||||||
@@ -41,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> {
|
||||||
@@ -85,4 +87,8 @@ class FakeEnterpriseService(
|
|||||||
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
||||||
return overrideWellKnownResult()
|
return overrideWellKnownResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun essConfigEndpointUrl(domain: String): String {
|
||||||
|
return essConfigEndpointUrlResult(domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
@@ -14,12 +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.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
|
||||||
@@ -204,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
|
||||||
|
|||||||
+127
-5
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -15,9 +17,11 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
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.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
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
|
||||||
@@ -25,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
|
||||||
@@ -37,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
|
||||||
@@ -71,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
|
||||||
@@ -151,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),
|
||||||
)
|
)
|
||||||
@@ -316,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)
|
||||||
@@ -339,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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,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
|
||||||
|
|||||||
+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()
|
||||||
}
|
}
|
||||||
|
|||||||
+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() },
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+4
-1
@@ -11,6 +11,9 @@ package io.element.android.features.invite.api
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers which invitations the user has already looked at, so that only genuinely new ones are highlighted in the room list.
|
||||||
|
*/
|
||||||
interface SeenInvitesStore {
|
interface SeenInvitesStore {
|
||||||
/**
|
/**
|
||||||
* Returns a flow of seen room IDs of invitation.
|
* Returns a flow of seen room IDs of invitation.
|
||||||
@@ -32,7 +35,7 @@ interface SeenInvitesStore {
|
|||||||
suspend fun markAsUnSeen(roomId: RoomId)
|
suspend fun markAsUnSeen(roomId: RoomId)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the store.
|
* Delete the store, so every invitation counts as unseen again.
|
||||||
*/
|
*/
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -10,6 +10,9 @@ package io.element.android.features.invite.api.acceptdecline
|
|||||||
|
|
||||||
import io.element.android.features.invite.api.InviteData
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
|
||||||
|
*/
|
||||||
interface AcceptDeclineInviteEvents {
|
interface AcceptDeclineInviteEvents {
|
||||||
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
|
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
|
||||||
|
|||||||
+13
@@ -12,7 +12,20 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the confirmation dialogs and error states of accepting or declining an invitation; it draws nothing until such an event is sent.
|
||||||
|
*
|
||||||
|
* Embedded by every screen that can act on an invitation, so the behaviour stays identical between the invite list and a room preview.
|
||||||
|
*/
|
||||||
fun interface AcceptDeclineInviteView {
|
fun interface AcceptDeclineInviteView {
|
||||||
|
/**
|
||||||
|
* Draws whichever dialog the current state calls for.
|
||||||
|
*
|
||||||
|
* @param state the state produced by the accept/decline presenter.
|
||||||
|
* @param onAcceptInviteSuccess called once the room has been joined, so the host screen can navigate into it.
|
||||||
|
* @param onDeclineInviteSuccess called once the invitation has been declined.
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: AcceptDeclineInviteState,
|
state: AcceptDeclineInviteState,
|
||||||
|
|||||||
+2
-2
@@ -81,7 +81,7 @@ fun DeclineAndBlockView(
|
|||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title))
|
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title))
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
@@ -96,7 +96,7 @@ fun DeclineAndBlockView(
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(text = stringResource(CommonStrings.action_report_room))
|
Text(text = stringResource(CommonStrings.action_report_room))
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
|
|||||||
+3
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
|
||||||
|
*/
|
||||||
interface InvitePeopleEvents {
|
interface InvitePeopleEvents {
|
||||||
data object SendInvites : InvitePeopleEvents
|
data object SendInvites : InvitePeopleEvents
|
||||||
data object CloseSearch : InvitePeopleEvents
|
data object CloseSearch : InvitePeopleEvents
|
||||||
|
|||||||
+7
@@ -13,7 +13,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
|
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
|
||||||
|
/**
|
||||||
|
* Creates a presenter for one invite flow.
|
||||||
|
*/
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
/**
|
||||||
|
* @param joinedRoom the room to invite into, or `null` when the room does not exist yet, as when inviting from a direct message.
|
||||||
|
* @param roomId the id of the room to invite into.
|
||||||
|
*/
|
||||||
fun create(
|
fun create(
|
||||||
joinedRoom: JoinedRoom?,
|
joinedRoom: JoinedRoom?,
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
|
|||||||
+9
@@ -11,7 +11,16 @@ package io.element.android.features.invitepeople.api
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the invite people UI, so that other screens can embed it without depending on its implementation.
|
||||||
|
*/
|
||||||
interface InvitePeopleRenderer {
|
interface InvitePeopleRenderer {
|
||||||
|
/**
|
||||||
|
* Draws the invite list and its search field.
|
||||||
|
*
|
||||||
|
* @param state the state produced by [InvitePeoplePresenter].
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: InvitePeopleState,
|
state: InvitePeopleState,
|
||||||
|
|||||||
+12
@@ -11,10 +11,22 @@ package io.element.android.features.invitepeople.api
|
|||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the invite people UI, produced by an [InvitePeoplePresenter] and consumed by an [InvitePeopleRenderer].
|
||||||
|
*/
|
||||||
interface InvitePeopleState {
|
interface InvitePeopleState {
|
||||||
|
/** Whether the send button should be enabled, i.e. at least one user is selected and the user has the permission. */
|
||||||
val canInvite: Boolean
|
val canInvite: Boolean
|
||||||
|
|
||||||
|
/** Whether the search field currently has focus, which the host screen uses to adapt its own layout. */
|
||||||
val isSearchActive: Boolean
|
val isSearchActive: Boolean
|
||||||
|
|
||||||
|
/** Progress of the invite request, so the UI can show a loader and surface failures. */
|
||||||
val sendInvitesAction: AsyncAction<Unit>
|
val sendInvitesAction: AsyncAction<Unit>
|
||||||
|
|
||||||
|
/** Progress of turning a direct message into a room, which happens when inviting a third person into a DM. */
|
||||||
val createRoomFromDmAction: AsyncAction<RoomId>
|
val createRoomFromDmAction: AsyncAction<RoomId>
|
||||||
|
|
||||||
|
/** Where the UI sends its events. */
|
||||||
val eventSink: (InvitePeopleEvents) -> Unit
|
val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +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_invite_users_already_a_member">"Bereits Mitglied"</string>
|
<string name="screen_invite_users_already_a_member">"Bereits Mitglied"</string>
|
||||||
<string name="screen_invite_users_already_invited">"Bereits eingeladen"</string>
|
<string name="screen_invite_users_already_invited">"Bereits eingeladen"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Du hast derzeit keine Chats mit diesen Kontakten. Bestätige, dass du sie in diesen Chat einladen möchtest, bevor du fortfährst."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Du hast derzeit keine Chats mit diesem Kontakt. Bestätige, dass du ihn in diesen Chat einladen möchtest, bevor du fortfährst."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Möchtest du neue Kontakte in diesen Chat einladen?"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_one_user">"Willst du einen neuen Kontakt in diesen Chat einladen?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,4 +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_invite_users_already_a_member">"已是成員"</string>
|
<string name="screen_invite_users_already_a_member">"已是成員"</string>
|
||||||
<string name="screen_invite_users_already_invited">"已邀請"</string>
|
<string name="screen_invite_users_already_invited">"已邀請"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"您目前與這些聯絡人沒有任何聊天紀錄。請確認是否要邀請他們加入此聊天室,然後再繼續。"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"您目前與此聯絡人沒有任何聊天紀錄。請確認是否要邀請對方加入此聊天室,然後再繼續。"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"要邀請新聯絡人加入此聊天室嗎?"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_one_user">"要邀請新聯絡人加入此聊天室嗎?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+9
@@ -11,7 +11,16 @@ package io.element.android.features.knockrequests.api.banner
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the banner shown at the top of a room when people are waiting to join it.
|
||||||
|
*/
|
||||||
interface KnockRequestsBannerRenderer {
|
interface KnockRequestsBannerRenderer {
|
||||||
|
/**
|
||||||
|
* Draws the banner, or nothing when there is no pending request or the user cannot moderate the room.
|
||||||
|
*
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
|
* @param onViewRequestsClick called when the user asks to see the full list of requests.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
|
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -10,6 +10,9 @@ package io.element.android.features.leaveroom.api
|
|||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events sent to the leave room presenter through [LeaveRoomState.eventSink].
|
||||||
|
*/
|
||||||
interface LeaveRoomEvent {
|
interface LeaveRoomEvent {
|
||||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
|
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -12,7 +12,17 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the confirmation dialogs of the leave room flow; it draws nothing until the host screen sends a leave event.
|
||||||
|
*/
|
||||||
fun interface LeaveRoomRenderer {
|
fun interface LeaveRoomRenderer {
|
||||||
|
/**
|
||||||
|
* Draws whichever dialog the current state calls for.
|
||||||
|
*
|
||||||
|
* @param state the state produced by the leave room presenter.
|
||||||
|
* @param onSelectNewOwners called when the user is the last owner of a room that still has other members, and must promote someone before leaving.
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: LeaveRoomState,
|
state: LeaveRoomState,
|
||||||
|
|||||||
+4
@@ -10,7 +10,11 @@ package io.element.android.features.leaveroom.api
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the leave room flow, which is only the confirmation dialogs: the screen it is hosted in owns the rest of the UI.
|
||||||
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
interface LeaveRoomState {
|
interface LeaveRoomState {
|
||||||
|
/** Where the host screen sends its events to start or confirm leaving a room. */
|
||||||
val eventSink: (LeaveRoomEvent) -> Unit
|
val eventSink: (LeaveRoomEvent) -> Unit
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ fun DependenciesDetailsView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
ClickableLinkText(
|
ClickableLinkText(
|
||||||
text = text,
|
text = text,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ fun DependencyLicensesListView(
|
|||||||
}
|
}
|
||||||
is AsyncData.Success -> items(state.licenses.data) { license ->
|
is AsyncData.Success -> items(state.licenses.data) { license ->
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(license.safeName) },
|
content = { Text(license.safeName) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
buildString {
|
buildString {
|
||||||
|
|||||||
+17
-5
@@ -12,10 +12,12 @@ import dev.zacsweers.metro.SingleIn
|
|||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
|
||||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
|
||||||
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -23,6 +25,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("LinkNewDesktopHandler", LoggerTags.linkNewDevice)
|
private val loggerTag = LoggerTag("LinkNewDesktopHandler", LoggerTags.linkNewDevice)
|
||||||
|
|
||||||
@@ -40,25 +43,34 @@ class LinkNewDesktopHandler(
|
|||||||
get() = linkDesktopStepFlow.asStateFlow()
|
get() = linkDesktopStepFlow.asStateFlow()
|
||||||
|
|
||||||
private var currentJob: Job? = null
|
private var currentJob: Job? = null
|
||||||
|
private var timerJob: Job? = null
|
||||||
private var handler: LinkDesktopHandler? = null
|
private var handler: LinkDesktopHandler? = null
|
||||||
|
|
||||||
fun createNewHandler() {
|
private fun resetJobs() {
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
currentJob = null
|
currentJob = null
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNewHandler() {
|
||||||
|
resetJobs()
|
||||||
handler = matrixClient.createLinkDesktopHandler().getOrNull()
|
handler = matrixClient.createLinkDesktopHandler().getOrNull()
|
||||||
|
timerJob = sessionScope.launch {
|
||||||
|
delay(2.minutes)
|
||||||
|
linkDesktopStepFlow.emit(LinkDesktopStep.Error(ErrorType.Expired("Scanning QrCode took too long.")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
currentJob?.cancel()
|
resetJobs()
|
||||||
currentJob = null
|
|
||||||
sessionScope.launch {
|
sessionScope.launch {
|
||||||
linkDesktopStepFlow.emit(LinkDesktopStep.Uninitialized)
|
linkDesktopStepFlow.emit(LinkDesktopStep.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onScannedCode(data: ByteArray) {
|
fun onScannedCode(data: ByteArray) {
|
||||||
currentJob?.cancel()
|
resetJobs()
|
||||||
currentJob = null
|
|
||||||
val currentHandler = handler
|
val currentHandler = handler
|
||||||
if (currentHandler == null) {
|
if (currentHandler == null) {
|
||||||
Timber.tag(loggerTag.value).e("onScannedCode: Handler is not initialized. Call createNewHandler() first.")
|
Timber.tag(loggerTag.value).e("onScannedCode: Handler is not initialized. Call createNewHandler() first.")
|
||||||
|
|||||||
+14
@@ -56,6 +56,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ class LinkNewDeviceFlowNode(
|
|||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
|
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
|
||||||
},
|
},
|
||||||
|
onResume = ::onResume,
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
linkMobileHandlerJob?.cancel()
|
linkMobileHandlerJob?.cancel()
|
||||||
linkDesktopHandlerJob?.cancel()
|
linkDesktopHandlerJob?.cancel()
|
||||||
@@ -209,6 +211,18 @@ class LinkNewDeviceFlowNode(
|
|||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onResume() = sessionCoroutineScope.launch {
|
||||||
|
// Application is resumed, if the step is waiting for auth, send the confirmation
|
||||||
|
(linkNewMobileHandler.stepFlow.value as? LinkMobileStep.WaitingForAuth)?.let {
|
||||||
|
Timber.tag(tag.value).d("Resuming while waiting for auth on mobile, sending confirmation")
|
||||||
|
it.continuationMessageSender.confirm()
|
||||||
|
}
|
||||||
|
(linkNewDesktopHandler.stepFlow.value as? LinkDesktopStep.WaitingForAuth)?.let {
|
||||||
|
Timber.tag(tag.value).d("Resuming while waiting for auth on desktop, sending confirmation")
|
||||||
|
it.continuationMessageSender.confirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateToError(errorType: ErrorType) {
|
private fun navigateToError(errorType: ErrorType) {
|
||||||
// Map the error to an error screen
|
// Map the error to an error screen
|
||||||
val error = when (errorType) {
|
val error = when (errorType) {
|
||||||
|
|||||||
+8
@@ -20,6 +20,13 @@ import io.element.android.libraries.architecture.callback
|
|||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max number of QrCode rotation.
|
||||||
|
* When QrCode is rotated this number of time, a TimeOut error is emitted.
|
||||||
|
* Set to 0 to disable QrCode rotation.
|
||||||
|
*/
|
||||||
|
private const val MAX_QR_CODE_ROTATION = 0
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ShowQrCodeNode(
|
class ShowQrCodeNode(
|
||||||
@@ -39,6 +46,7 @@ class ShowQrCodeNode(
|
|||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
private val showQrCodePresenter: ShowQrCodePresenter = showQrCodePresenterFactory.create(
|
private val showQrCodePresenter: ShowQrCodePresenter = showQrCodePresenterFactory.create(
|
||||||
initialData = inputs.data,
|
initialData = inputs.data,
|
||||||
|
maxQrCodeRotation = MAX_QR_CODE_ROTATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+8
-7
@@ -26,24 +26,29 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice)
|
private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice)
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ShowQrCodePresenter(
|
class ShowQrCodePresenter(
|
||||||
@Assisted private val initialData: String,
|
@Assisted private val initialData: String,
|
||||||
|
@Assisted private val maxQrCodeRotation: Int,
|
||||||
private val linkNewMobileHandler: LinkNewMobileHandler,
|
private val linkNewMobileHandler: LinkNewMobileHandler,
|
||||||
) : Presenter<ShowQrCodeState> {
|
) : Presenter<ShowQrCodeState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialData: String): ShowQrCodePresenter
|
fun create(
|
||||||
|
initialData: String,
|
||||||
|
maxQrCodeRotation: Int,
|
||||||
|
): ShowQrCodePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ShowQrCodeState {
|
override fun present(): ShowQrCodeState {
|
||||||
var qrCodeRotationCounter by remember { mutableIntStateOf(MAX_QR_CODE_ROTATION) }
|
var qrCodeRotationCounter by remember { mutableIntStateOf(maxQrCodeRotation) }
|
||||||
val state by produceState(
|
val state by produceState(
|
||||||
initialValue = ShowQrCodeState(
|
initialValue = ShowQrCodeState(
|
||||||
data = AsyncData.Success(initialData),
|
data = AsyncData.Success(initialData),
|
||||||
@@ -63,7 +68,7 @@ class ShowQrCodePresenter(
|
|||||||
linkNewMobileHandler.rotateQrCode()
|
linkNewMobileHandler.rotateQrCode()
|
||||||
// Ensure that outdated data is not rendered too long while rotating QR code
|
// Ensure that outdated data is not rendered too long while rotating QR code
|
||||||
loadingJob = launch {
|
loadingJob = launch {
|
||||||
delay(1000)
|
delay(1.seconds)
|
||||||
value = ShowQrCodeState(
|
value = ShowQrCodeState(
|
||||||
data = AsyncData.Loading(),
|
data = AsyncData.Loading(),
|
||||||
)
|
)
|
||||||
@@ -80,8 +85,4 @@ class ShowQrCodePresenter(
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val MAX_QR_CODE_ROTATION = 10
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Skaneeri QR-koodi selle seadmega"</string>
|
<string name="screen_link_new_device_desktop_step3">"Skaneeri QR-koodi selle seadmega"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Skaneerimiseks valmis"</string>
|
<string name="screen_link_new_device_desktop_submit">"Skaneerimiseks valmis"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"QR-koodi laadimiseks ava %1$s süle- või lauaarvutis"</string>
|
<string name="screen_link_new_device_desktop_title">"QR-koodi laadimiseks ava %1$s süle- või lauaarvutis"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Skaneeri QR-koodi peale järgnevate sammude läbitegemist oma seadmes. Ära iialgi skaneeri QR-koodi, mille on saatnud keegi teine."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numbrid ei klapi"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numbrid ei klapi"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Sisesta kahekohaline kood"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Sisesta kahekohaline kood"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Sellega verifitseerime, et ühendus sinu teise seadmega on turvaline."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Sellega verifitseerime, et ühendus sinu teise seadmega on turvaline."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Skenirajte QR kod ovim uređajem"</string>
|
<string name="screen_link_new_device_desktop_step3">"Skenirajte QR kod ovim uređajem"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Spremno za skeniranje"</string>
|
<string name="screen_link_new_device_desktop_submit">"Spremno za skeniranje"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Otvorite %1$s na stolnom računalu kako biste dobili QR kod"</string>
|
<string name="screen_link_new_device_desktop_title">"Otvorite %1$s na stolnom računalu kako biste dobili QR kod"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"QR kod skenirajte tek nakon što ste na vlastitom uređaju slijedili gore navedene korake. Nikada ne skenirajte kod koji vam je poslala druga osoba."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Brojevi se ne podudaraju"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Brojevi se ne podudaraju"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Unesite dvoznamenkasti kod"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Unesite dvoznamenkasti kod"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Time ćete potvrditi da je veza s vašim drugim uređajem sigurna."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Time ćete potvrditi da je veza s vašim drugim uređajem sigurna."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
|
<string name="screen_link_new_device_desktop_step3">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Készen áll a beolvasásra"</string>
|
<string name="screen_link_new_device_desktop_submit">"Készen áll a beolvasásra"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Nyissa meg az asztali %1$s alkalmazást, hogy megkapja a QR-kódot"</string>
|
<string name="screen_link_new_device_desktop_title">"Nyissa meg az asztali %1$s alkalmazást, hogy megkapja a QR-kódot"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Csak a fenti lépések végrehajtása után olvass be QR-kódot a saját eszközén. Soha ne olvasson be más által küldött kódot."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"A számok nem egyeznek"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"A számok nem egyeznek"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Írja be a kétjegyű kódot"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Írja be a kétjegyű kódot"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Ezzel ellenőrizni fogja, hogy a másik eszközzel való kapcsolat biztonságos-e."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Ezzel ellenőrizni fogja, hogy a másik eszközzel való kapcsolat biztonságos-e."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Scanați codul QR cu acest dispozitiv"</string>
|
<string name="screen_link_new_device_desktop_step3">"Scanați codul QR cu acest dispozitiv"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Gata de scanare"</string>
|
<string name="screen_link_new_device_desktop_submit">"Gata de scanare"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Deschide %1$s pe un computer desktop pentru a obține codul QR"</string>
|
<string name="screen_link_new_device_desktop_title">"Deschide %1$s pe un computer desktop pentru a obține codul QR"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Scanați un cod QR doar după ce urmați pașii de mai sus pe propriul dispozitiv. Nu scanați niciodată un cod trimis de o altă persoană."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numerele nu se potrivesc"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numerele nu se potrivesc"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Introduceți codul de 2 cifre"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Introduceți codul de 2 cifre"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Aceasta va verifica dacă conexiunea cu celălalt dispozitiv este sigură."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Aceasta va verifica dacă conexiunea cu celălalt dispozitiv este sigură."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"使用此裝置掃描 QR code"</string>
|
<string name="screen_link_new_device_desktop_step3">"使用此裝置掃描 QR code"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"準備掃描"</string>
|
<string name="screen_link_new_device_desktop_submit">"準備掃描"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"在桌上型電腦上開啟 %1$s 以取得 QR code"</string>
|
<string name="screen_link_new_device_desktop_title">"在桌上型電腦上開啟 %1$s 以取得 QR code"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"請務必先在您的裝置上完成上述步驟,之後才掃描 QR code。切勿掃描他人傳送的 QR code。"</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"數字不符"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"數字不符"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"輸入兩位數代碼"</string>
|
<string name="screen_link_new_device_enter_number_notice">"輸入兩位數代碼"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"這將確認您與另一台裝置之間的連線是否安全。"</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"這將確認您與另一台裝置之間的連線是否安全。"</string>
|
||||||
|
|||||||
+26
@@ -91,10 +91,36 @@ class ShowQrCodePresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when handler emits QrRotating, the presenter does not emit anything when rotation is disabled`() = runTest {
|
||||||
|
val linkMobileHandler = FakeLinkMobileHandler(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
createLinkMobileHandlerResult = { Result.success(linkMobileHandler) },
|
||||||
|
)
|
||||||
|
val linkNewMobileHandler = LinkNewMobileHandler(matrixClient)
|
||||||
|
linkNewMobileHandler.createAndStartNewHandler()
|
||||||
|
createPresenter(
|
||||||
|
linkNewMobileHandler = linkNewMobileHandler,
|
||||||
|
maxQrCodeRotation = 0,
|
||||||
|
).test {
|
||||||
|
awaitItem()
|
||||||
|
linkMobileHandler.emitStep(
|
||||||
|
LinkMobileStep.QrRotating
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
linkNewMobileHandler: LinkNewMobileHandler = LinkNewMobileHandler(FakeMatrixClient()),
|
linkNewMobileHandler: LinkNewMobileHandler = LinkNewMobileHandler(FakeMatrixClient()),
|
||||||
|
maxQrCodeRotation: Int = 10,
|
||||||
) = ShowQrCodePresenter(
|
) = ShowQrCodePresenter(
|
||||||
initialData = "DATA",
|
initialData = "DATA",
|
||||||
|
maxQrCodeRotation = maxQrCodeRotation,
|
||||||
linkNewMobileHandler = linkNewMobileHandler,
|
linkNewMobileHandler = linkNewMobileHandler,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
||||||
|
|||||||
+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.location.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a [MapTilerConfig] for the MapLibre maps.
|
||||||
|
*/
|
||||||
|
val LocalMapTilerConfig = staticCompositionLocalOf {
|
||||||
|
MapTilerConfig(
|
||||||
|
apiKey = BuildConfig.MAPTILER_API_KEY,
|
||||||
|
lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
|
||||||
|
darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID,
|
||||||
|
baseUrl = BuildConfig.MAPTILER_BASE_URL
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.api
|
package io.element.android.features.location.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether the location features can be used at all, which depends on the build having a map provider configured.
|
||||||
|
*/
|
||||||
interface LocationService {
|
interface LocationService {
|
||||||
|
/** Whether location sharing and map rendering are available; `false` disables the whole feature in the UI. */
|
||||||
fun isServiceAvailable(): Boolean
|
fun isServiceAvailable(): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -38,13 +38,14 @@ import coil3.request.ImageRequest
|
|||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||||
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
|
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
|
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
|
||||||
import io.element.android.libraries.designsystem.components.LocationPin
|
import io.element.android.libraries.designsystem.components.LocationPin
|
||||||
import io.element.android.libraries.designsystem.components.PinVariant
|
import io.element.android.libraries.designsystem.components.PinVariant
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a static map image downloaded via a third party service's static maps API.
|
* Shows a static map image downloaded via a third party service's static maps API.
|
||||||
@@ -57,6 +58,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun StaticMapView(
|
fun StaticMapView(
|
||||||
|
mapTilerConfig: MapTilerConfig?,
|
||||||
location: Location?,
|
location: Location?,
|
||||||
zoom: Double,
|
zoom: Double,
|
||||||
pinVariant: PinVariant,
|
pinVariant: PinVariant,
|
||||||
@@ -94,6 +96,7 @@ fun StaticMapView(
|
|||||||
}
|
}
|
||||||
// Cases 3 & 4: Non-null location - fetch map
|
// Cases 3 & 4: Non-null location - fetch map
|
||||||
else -> LoadableMapContent(
|
else -> LoadableMapContent(
|
||||||
|
mapTilerConfig = mapTilerConfig,
|
||||||
location = location,
|
location = location,
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
pinVariant = pinVariant,
|
pinVariant = pinVariant,
|
||||||
@@ -124,6 +127,7 @@ private fun BoxWithConstraintsScope.StaleMapContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxWithConstraintsScope.LoadableMapContent(
|
private fun BoxWithConstraintsScope.LoadableMapContent(
|
||||||
|
mapTilerConfig: MapTilerConfig?,
|
||||||
location: Location,
|
location: Location,
|
||||||
zoom: Double,
|
zoom: Double,
|
||||||
pinVariant: PinVariant,
|
pinVariant: PinVariant,
|
||||||
@@ -132,7 +136,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var retryHash by remember { mutableIntStateOf(0) }
|
var retryHash by remember { mutableIntStateOf(0) }
|
||||||
val builder = remember { StaticMapUrlBuilder() }
|
val builder = rememberStaticMapBuilder(mapTilerConfig)
|
||||||
|
|
||||||
val (painter, state, contentScale) = if (LocalInspectionMode.current) {
|
val (painter, state, contentScale) = if (LocalInspectionMode.current) {
|
||||||
val painter = painterResource(CommonDrawables.sample_map)
|
val painter = painterResource(CommonDrawables.sample_map)
|
||||||
@@ -203,6 +207,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun StaticMapViewPreview() = ElementPreview {
|
internal fun StaticMapViewPreview() = ElementPreview {
|
||||||
StaticMapView(
|
StaticMapView(
|
||||||
|
mapTilerConfig = null,
|
||||||
location = Location(0.0, 0.0),
|
location = Location(0.0, 0.0),
|
||||||
zoom = 0.0,
|
zoom = 0.0,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
+8
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
import io.element.android.features.location.api.BuildConfig
|
import io.element.android.features.location.api.BuildConfig
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,13 @@ internal class MapTilerStaticMapUrlBuilder(
|
|||||||
darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID,
|
darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
constructor(mapTilerConfig: MapTilerConfig) : this(
|
||||||
|
baseUrl = (mapTilerConfig.baseUrl ?: BuildConfig.MAPTILER_BASE_URL).removeSuffix("/"),
|
||||||
|
apiKey = mapTilerConfig.apiKey.takeIf { it.isNotBlank() } ?: BuildConfig.MAPTILER_API_KEY,
|
||||||
|
lightMapId = mapTilerConfig.lightStyleId ?: BuildConfig.MAPTILER_LIGHT_MAP_ID,
|
||||||
|
darkMapId = mapTilerConfig.darkStyleId ?: BuildConfig.MAPTILER_DARK_MAP_ID,
|
||||||
|
)
|
||||||
|
|
||||||
override fun build(
|
override fun build(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
|
|||||||
+10
-9
@@ -11,6 +11,7 @@
|
|||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
import io.element.android.features.location.api.BuildConfig
|
import io.element.android.features.location.api.BuildConfig
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
|
|
||||||
internal class MapTilerTileServerStyleUriBuilder(
|
internal class MapTilerTileServerStyleUriBuilder(
|
||||||
private val baseUrl: String,
|
private val baseUrl: String,
|
||||||
@@ -26,17 +27,17 @@ internal class MapTilerTileServerStyleUriBuilder(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override fun build(
|
override fun build(
|
||||||
customMapStyleUrl: String?,
|
customMapTilerConfig: MapTilerConfig?,
|
||||||
darkMode: Boolean,
|
darkMode: Boolean,
|
||||||
): String {
|
): String {
|
||||||
return buildString {
|
val baseUrl = customMapTilerConfig?.baseUrl.takeIf { !it.isNullOrBlank() } ?: baseUrl
|
||||||
if (customMapStyleUrl.isNullOrBlank()) {
|
val apiKey = customMapTilerConfig?.apiKey ?: apiKey
|
||||||
val mapId = if (darkMode) darkMapId else lightMapId
|
val mapId = if (darkMode) {
|
||||||
append("$baseUrl/$mapId/style.json")
|
customMapTilerConfig?.darkStyleId ?: darkMapId
|
||||||
} else {
|
} else {
|
||||||
append(customMapStyleUrl)
|
customMapTilerConfig?.lightStyleId ?: lightMapId
|
||||||
}
|
|
||||||
append("?key=$apiKey")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "$baseUrl/$mapId/style.json?key=$apiKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-1
@@ -8,10 +8,25 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an URL for a 3rd party service provider static maps API.
|
* Builds an URL for a 3rd party service provider static maps API.
|
||||||
*/
|
*/
|
||||||
interface StaticMapUrlBuilder {
|
interface StaticMapUrlBuilder {
|
||||||
|
/**
|
||||||
|
* Builds the URL of a map image centred on the given coordinates.
|
||||||
|
*
|
||||||
|
* @param lat latitude of the centre of the map.
|
||||||
|
* @param lon longitude of the centre of the map.
|
||||||
|
* @param zoom zoom level to render at.
|
||||||
|
* @param darkMode whether to request the dark map style.
|
||||||
|
* @param width width of the image in density independent pixels.
|
||||||
|
* @param height height of the image in density independent pixels.
|
||||||
|
* @param density screen density, used to request a matching pixel size.
|
||||||
|
*/
|
||||||
fun build(
|
fun build(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lon: Double,
|
lon: Double,
|
||||||
@@ -22,7 +37,17 @@ interface StaticMapUrlBuilder {
|
|||||||
density: Float,
|
density: Float,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
/** Whether a map provider is configured for this build; the URLs are unusable when it is not. */
|
||||||
fun isServiceAvailable(): Boolean
|
fun isServiceAvailable(): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder()
|
@Composable
|
||||||
|
fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder {
|
||||||
|
return remember(mapTilerConfig) {
|
||||||
|
if (mapTilerConfig != null) {
|
||||||
|
MapTilerStaticMapUrlBuilder(mapTilerConfig)
|
||||||
|
} else {
|
||||||
|
MapTilerStaticMapUrlBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-4
@@ -11,6 +11,7 @@ package io.element.android.features.location.api.internal
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a style URI for a MapLibre compatible tile server.
|
* Builds a style URI for a MapLibre compatible tile server.
|
||||||
@@ -18,8 +19,14 @@ import io.element.android.compound.theme.ElementTheme
|
|||||||
* Used for rendering dynamic maps.
|
* Used for rendering dynamic maps.
|
||||||
*/
|
*/
|
||||||
interface TileServerStyleUriBuilder {
|
interface TileServerStyleUriBuilder {
|
||||||
|
/**
|
||||||
|
* Builds the style URI the map view loads its tiles from.
|
||||||
|
*
|
||||||
|
* @param customMapStyleUrl the style advertised by the homeserver, or `null` to use the one built into the app.
|
||||||
|
* @param darkMode whether to request the dark map style.
|
||||||
|
*/
|
||||||
fun build(
|
fun build(
|
||||||
customMapStyleUrl: String?,
|
customMapTilerConfig: MapTilerConfig?,
|
||||||
darkMode: Boolean,
|
darkMode: Boolean,
|
||||||
): String
|
): String
|
||||||
}
|
}
|
||||||
@@ -31,12 +38,12 @@ interface TileServerStyleUriBuilder {
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberTileStyleUrl(
|
fun rememberTileStyleUrl(
|
||||||
customMapStyleUrl: String?,
|
customMapTileConfig: MapTilerConfig?,
|
||||||
): String {
|
): String {
|
||||||
val darkMode = !ElementTheme.isLightTheme
|
val darkMode = !ElementTheme.isLightTheme
|
||||||
return remember(darkMode, customMapStyleUrl) {
|
return remember(darkMode, customMapTileConfig) {
|
||||||
MapTilerTileServerStyleUriBuilder().build(
|
MapTilerTileServerStyleUriBuilder().build(
|
||||||
customMapStyleUrl = customMapStyleUrl,
|
customMapTilerConfig = customMapTileConfig,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -12,6 +12,9 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the live location shares running on this device, across every room, and the foreground service that feeds them GPS updates.
|
||||||
|
*/
|
||||||
interface ActiveLiveLocationShareManager {
|
interface ActiveLiveLocationShareManager {
|
||||||
/** All rooms currently sharing live location on this device. */
|
/** All rooms currently sharing live location on this device. */
|
||||||
val sharingRoomIds: StateFlow<Set<RoomId>>
|
val sharingRoomIds: StateFlow<Set<RoomId>>
|
||||||
@@ -27,6 +30,9 @@ interface ActiveLiveLocationShareManager {
|
|||||||
* Starts live location sharing in the given room.
|
* Starts live location sharing in the given room.
|
||||||
* Calls room.startLiveLocationShare() on the SDK, registers the share,
|
* Calls room.startLiveLocationShare() on the SDK, registers the share,
|
||||||
* and starts the foreground GPS service if not already running.
|
* and starts the foreground GPS service if not already running.
|
||||||
|
*
|
||||||
|
* @param roomId the room to start sharing the location in.
|
||||||
|
* @param duration how long the share should last before it expires on its own.
|
||||||
*/
|
*/
|
||||||
suspend fun startShare(roomId: RoomId, duration: Duration): Result<Unit>
|
suspend fun startShare(roomId: RoomId, duration: Duration): Result<Unit>
|
||||||
|
|
||||||
@@ -34,6 +40,8 @@ interface ActiveLiveLocationShareManager {
|
|||||||
* Stops live location sharing in the given room.
|
* Stops live location sharing in the given room.
|
||||||
* Calls room.stopLiveLocationShare() on the SDK, removes the share,
|
* Calls room.stopLiveLocationShare() on the SDK, removes the share,
|
||||||
* and stops the foreground service if no shares remain.
|
* and stops the foreground service if no shares remain.
|
||||||
|
*
|
||||||
|
* @param roomId the room to stop sharing the location in.
|
||||||
*/
|
*/
|
||||||
suspend fun stopShare(roomId: RoomId): Result<Unit>
|
suspend fun stopShare(roomId: RoomId): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-6
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class MapTilerTileServerStyleUriBuilderTest {
|
class MapTilerTileServerStyleUriBuilderTest {
|
||||||
@@ -23,7 +24,7 @@ class MapTilerTileServerStyleUriBuilderTest {
|
|||||||
fun `light map uri`() {
|
fun `light map uri`() {
|
||||||
assertThat(
|
assertThat(
|
||||||
builder.build(
|
builder.build(
|
||||||
customMapStyleUrl = null,
|
customMapTilerConfig = null,
|
||||||
darkMode = false,
|
darkMode = false,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
|
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
|
||||||
@@ -33,7 +34,7 @@ class MapTilerTileServerStyleUriBuilderTest {
|
|||||||
fun `dark map uri`() {
|
fun `dark map uri`() {
|
||||||
assertThat(
|
assertThat(
|
||||||
builder.build(
|
builder.build(
|
||||||
customMapStyleUrl = null,
|
customMapTilerConfig = null,
|
||||||
darkMode = true,
|
darkMode = true,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey")
|
).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey")
|
||||||
@@ -43,19 +44,44 @@ class MapTilerTileServerStyleUriBuilderTest {
|
|||||||
fun `custom map uri light`() {
|
fun `custom map uri light`() {
|
||||||
assertThat(
|
assertThat(
|
||||||
builder.build(
|
builder.build(
|
||||||
customMapStyleUrl = "https://custom.url/style.json",
|
customMapTilerConfig = MapTilerConfig(
|
||||||
|
apiKey = "API_KEY",
|
||||||
|
lightStyleId = "light",
|
||||||
|
darkStyleId = "dark",
|
||||||
|
baseUrl = "https://custom.url/style.json"
|
||||||
|
),
|
||||||
darkMode = false,
|
darkMode = false,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://custom.url/style.json?key=anApiKey")
|
).isEqualTo("https://custom.url/style.json/light/style.json?key=API_KEY")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `custom map uri dark`() {
|
fun `custom map uri dark`() {
|
||||||
assertThat(
|
assertThat(
|
||||||
builder.build(
|
builder.build(
|
||||||
customMapStyleUrl = "https://custom.url/style.json",
|
customMapTilerConfig = MapTilerConfig(
|
||||||
|
apiKey = "API_KEY",
|
||||||
|
lightStyleId = "light",
|
||||||
|
darkStyleId = "dark",
|
||||||
|
baseUrl = "https://custom.url/style.json"
|
||||||
|
),
|
||||||
darkMode = true,
|
darkMode = true,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://custom.url/style.json?key=anApiKey")
|
).isEqualTo("https://custom.url/style.json/dark/style.json?key=API_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank custom map uri falls back to the built-in style`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
customMapTilerConfig = MapTilerConfig(
|
||||||
|
apiKey = "API_KEY",
|
||||||
|
lightStyleId = "light",
|
||||||
|
darkStyleId = "dark",
|
||||||
|
baseUrl = " "
|
||||||
|
),
|
||||||
|
darkMode = false,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://base.url/light/style.json?key=API_KEY")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ setupDependencyInjection()
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.features.location.api)
|
api(projects.features.location.api)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(projects.features.messages.api)
|
implementation(projects.features.messages.api)
|
||||||
implementation(libs.maplibre.compose)
|
implementation(libs.maplibre.compose)
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
@@ -45,6 +46,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
||||||
@@ -58,5 +60,7 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.libraries.wellknown.test)
|
||||||
testImplementation(projects.features.location.test)
|
testImplementation(projects.features.location.test)
|
||||||
|
testImplementation(projects.features.enterprise.test)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -50,6 +50,7 @@ import io.element.android.libraries.core.data.tryOrNull
|
|||||||
import io.element.android.libraries.designsystem.text.toDp
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
import org.maplibre.compose.map.MapOptions
|
import org.maplibre.compose.map.MapOptions
|
||||||
@@ -66,7 +67,7 @@ import kotlin.math.roundToInt
|
|||||||
* - Updating camera position padding based on sheet height
|
* - Updating camera position padding based on sheet height
|
||||||
* - Rendering the MaplibreMap with proper ornament positioning
|
* - Rendering the MaplibreMap with proper ornament positioning
|
||||||
*
|
*
|
||||||
* @param customMapStyleUrl Optional custom style URL for the map
|
* @param customMapTilerConfig Optional custom style URL for the map
|
||||||
* @param modifier Modifier for the root layout
|
* @param modifier Modifier for the root layout
|
||||||
* @param scaffoldState State for the bottom sheet scaffold
|
* @param scaffoldState State for the bottom sheet scaffold
|
||||||
* @param cameraState The camera state for the map
|
* @param cameraState The camera state for the map
|
||||||
@@ -83,7 +84,7 @@ import kotlin.math.roundToInt
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MapBottomSheetScaffold(
|
fun MapBottomSheetScaffold(
|
||||||
customMapStyleUrl: AsyncData<String?>,
|
customMapTilerConfig: AsyncData<MapTilerConfig?>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||||
bottomSheetState = rememberBottomSheetState(
|
bottomSheetState = rememberBottomSheetState(
|
||||||
@@ -137,11 +138,11 @@ fun MapBottomSheetScaffold(
|
|||||||
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
|
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
|
||||||
val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions)
|
val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions)
|
||||||
Box {
|
Box {
|
||||||
when (customMapStyleUrl) {
|
when (customMapTilerConfig) {
|
||||||
is AsyncData.Success -> {
|
is AsyncData.Success -> {
|
||||||
MaplibreMap(
|
MaplibreMap(
|
||||||
options = mapOptions,
|
options = mapOptions,
|
||||||
baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapStyleUrl.data)),
|
baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapTilerConfig.data)),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
content = mapContent,
|
content = mapContent,
|
||||||
|
|||||||
+5
-3
@@ -22,6 +22,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.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
||||||
import io.element.android.features.location.impl.common.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
@@ -72,6 +73,7 @@ class ShareLocationPresenter(
|
|||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val liveLocationStore: LiveLocationStore,
|
private val liveLocationStore: LiveLocationStore,
|
||||||
private val userLocationStateFactory: UserLocationState.Factory,
|
private val userLocationStateFactory: UserLocationState.Factory,
|
||||||
|
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||||
) : Presenter<ShareLocationState> {
|
) : Presenter<ShareLocationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
@@ -92,9 +94,9 @@ class ShareLocationPresenter(
|
|||||||
var pendingLiveLocationShare by remember { mutableStateOf(false) }
|
var pendingLiveLocationShare by remember { mutableStateOf(false) }
|
||||||
val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
val currentUser by client.userProfile.collectAsState()
|
val currentUser by client.userProfile.collectAsState()
|
||||||
val customMapStyleUrl by produceState(AsyncData.Loading()) {
|
val customMapConfig by produceState(AsyncData.Loading()) {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
|
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig)
|
||||||
}
|
}
|
||||||
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
|
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
|
||||||
perms.sendLiveLocationPermissions()
|
perms.sendLiveLocationPermissions()
|
||||||
@@ -199,7 +201,7 @@ class ShareLocationPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ShareLocationState(
|
return ShareLocationState(
|
||||||
customMapStyleUrl = customMapStyleUrl,
|
customMapTilerConfig = customMapConfig,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
dialogState = dialogState,
|
dialogState = dialogState,
|
||||||
trackUserLocation = trackUserPosition,
|
trackUserLocation = trackUserPosition,
|
||||||
|
|||||||
+2
-1
@@ -13,10 +13,11 @@ import io.element.android.features.location.impl.common.userlocation.UserLocatio
|
|||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ShareLocationState(
|
data class ShareLocationState(
|
||||||
val customMapStyleUrl: AsyncData<String?>,
|
val customMapTilerConfig: AsyncData<MapTilerConfig?>,
|
||||||
val currentUser: MatrixUser,
|
val currentUser: MatrixUser,
|
||||||
val dialogState: Dialog,
|
val dialogState: Dialog,
|
||||||
val trackUserLocation: Boolean,
|
val trackUserLocation: Boolean,
|
||||||
|
|||||||
+5
-4
@@ -15,6 +15,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
|||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@@ -70,13 +71,13 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
|||||||
canShareLiveLocation = true,
|
canShareLiveLocation = true,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
customMapStyleUrl = AsyncData.Loading(),
|
customMapTilerConfig = AsyncData.Loading(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aShareLocationState(
|
fun aShareLocationState(
|
||||||
customMapStyleUrl: AsyncData<String?> = AsyncData.Success(null),
|
customMapTilerConfig: AsyncData<MapTilerConfig?> = AsyncData.Success(null),
|
||||||
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
||||||
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
|
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
|
||||||
trackUserPosition: Boolean = false,
|
trackUserPosition: Boolean = false,
|
||||||
@@ -87,7 +88,7 @@ fun aShareLocationState(
|
|||||||
eventSink: (ShareLocationEvent) -> Unit = {},
|
eventSink: (ShareLocationEvent) -> Unit = {},
|
||||||
): ShareLocationState {
|
): ShareLocationState {
|
||||||
return ShareLocationState(
|
return ShareLocationState(
|
||||||
customMapStyleUrl = customMapStyleUrl,
|
customMapTilerConfig = customMapTilerConfig,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
dialogState = dialogState,
|
dialogState = dialogState,
|
||||||
trackUserLocation = trackUserPosition,
|
trackUserLocation = trackUserPosition,
|
||||||
@@ -95,6 +96,6 @@ fun aShareLocationState(
|
|||||||
canShareLiveLocation = canShareLiveLocation,
|
canShareLiveLocation = canShareLiveLocation,
|
||||||
appName = appName,
|
appName = appName,
|
||||||
startLiveLocationAction = startLiveLocationAction,
|
startLiveLocationAction = startLiveLocationAction,
|
||||||
eventSink = eventSink
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -118,7 +118,7 @@ fun ShareLocationView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MapBottomSheetScaffold(
|
MapBottomSheetScaffold(
|
||||||
customMapStyleUrl = state.customMapStyleUrl,
|
customMapTilerConfig = state.customMapTilerConfig,
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
@@ -261,7 +261,7 @@ private fun ShareCurrentLocationItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(stringResource(CommonStrings.screen_share_my_location_action))
|
Text(stringResource(CommonStrings.screen_share_my_location_action))
|
||||||
},
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -276,7 +276,7 @@ private fun SharePinLocationItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(stringResource(CommonStrings.screen_share_this_location_action))
|
Text(stringResource(CommonStrings.screen_share_this_location_action))
|
||||||
},
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -291,7 +291,7 @@ private fun ShareLiveLocationItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(stringResource(CommonStrings.action_share_live_location))
|
Text(stringResource(CommonStrings.action_share_live_location))
|
||||||
},
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
+7
-5
@@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import dev.zacsweers.metro.Assisted
|
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 io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
import io.element.android.features.location.api.ShowLocationMode
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
@@ -46,7 +47,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
|
|||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||||
@@ -64,10 +65,11 @@ import kotlinx.coroutines.launch
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val client: MatrixClient,
|
private val sessionId: SessionId,
|
||||||
private val joinedRoom: JoinedRoom,
|
private val joinedRoom: JoinedRoom,
|
||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val userLocationStateFactory: UserLocationState.Factory,
|
private val userLocationStateFactory: UserLocationState.Factory,
|
||||||
|
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||||
) : Presenter<ShowLocationState> {
|
) : Presenter<ShowLocationState> {
|
||||||
@AssistedFactory fun interface Factory {
|
@AssistedFactory fun interface Factory {
|
||||||
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
||||||
@@ -85,9 +87,9 @@ import kotlinx.coroutines.launch
|
|||||||
mutableStateOf(LocationConstraintsDialogState.None)
|
mutableStateOf(LocationConstraintsDialogState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val customMapStyleUrl by produceState(AsyncData.Loading()) {
|
val customMapTilerConfig by produceState(AsyncData.Loading()) {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
|
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkLocationConstraints() {
|
fun checkLocationConstraints() {
|
||||||
@@ -220,7 +222,7 @@ import kotlinx.coroutines.launch
|
|||||||
userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted)
|
userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted)
|
||||||
}
|
}
|
||||||
return ShowLocationState(
|
return ShowLocationState(
|
||||||
customMapStyleUrl = customMapStyleUrl,
|
customMapTilerConfig = customMapTilerConfig,
|
||||||
dialogState = dialogState,
|
dialogState = dialogState,
|
||||||
locationShares = locationShares,
|
locationShares = locationShares,
|
||||||
focusedLocation = focusedLocation,
|
focusedLocation = focusedLocation,
|
||||||
|
|||||||
+2
-1
@@ -17,10 +17,11 @@ import io.element.android.libraries.designsystem.components.PinVariant
|
|||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ShowLocationState(
|
data class ShowLocationState(
|
||||||
val customMapStyleUrl: AsyncData<String?>,
|
val customMapTilerConfig: AsyncData<MapTilerConfig?>,
|
||||||
val isLive: Boolean,
|
val isLive: Boolean,
|
||||||
val dialogState: LocationConstraintsDialogState,
|
val dialogState: LocationConstraintsDialogState,
|
||||||
val locationShares: ImmutableList<LocationShareItem>,
|
val locationShares: ImmutableList<LocationShareItem>,
|
||||||
|
|||||||
+4
-3
@@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
|
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
@@ -36,14 +37,14 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
|||||||
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
|
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
|
||||||
),
|
),
|
||||||
aShowLocationState(isTrackMyLocation = true),
|
aShowLocationState(isTrackMyLocation = true),
|
||||||
aShowLocationState(customMapStyleUrl = AsyncData.Loading()),
|
aShowLocationState(customMapTilerConfig = AsyncData.Loading()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val APP_NAME = "ApplicationName"
|
private const val APP_NAME = "ApplicationName"
|
||||||
|
|
||||||
fun aShowLocationState(
|
fun aShowLocationState(
|
||||||
customMapStyleUrl: AsyncData<String?> = AsyncData.Success(null),
|
customMapTilerConfig: AsyncData<MapTilerConfig?> = AsyncData.Success(null),
|
||||||
isLive: Boolean = false,
|
isLive: Boolean = false,
|
||||||
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
|
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
|
||||||
locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
|
locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
|
||||||
@@ -55,7 +56,7 @@ fun aShowLocationState(
|
|||||||
eventSink: (ShowLocationEvent) -> Unit = {},
|
eventSink: (ShowLocationEvent) -> Unit = {},
|
||||||
): ShowLocationState {
|
): ShowLocationState {
|
||||||
return ShowLocationState(
|
return ShowLocationState(
|
||||||
customMapStyleUrl = customMapStyleUrl,
|
customMapTilerConfig = customMapTilerConfig,
|
||||||
dialogState = constraintsDialogState,
|
dialogState = constraintsDialogState,
|
||||||
locationShares = locationShares.toImmutableList(),
|
locationShares = locationShares.toImmutableList(),
|
||||||
focusedLocation = focusedLocation,
|
focusedLocation = focusedLocation,
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ fun ShowLocationView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MapBottomSheetScaffold(
|
MapBottomSheetScaffold(
|
||||||
customMapStyleUrl = state.customMapStyleUrl,
|
customMapTilerConfig = state.customMapTilerConfig,
|
||||||
sheetDragHandle = if (state.isSheetDraggable) {
|
sheetDragHandle = if (state.isSheetDraggable) {
|
||||||
{ BottomSheetDefaults.DragHandle() }
|
{ BottomSheetDefaults.DragHandle() }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?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_share_location_live_location_disclaimer_title">"Der Verlauf deines Live-Standorts wird im Chat gespeichert und bleibt für die Mitglieder sichtbar, auch nachdem du die Freigabe beendet hast."</string>
|
||||||
<string name="screen_share_location_live_location_duration_picker_title">"Wie lange soll der Live-Standort geteilt werden?"</string>
|
<string name="screen_share_location_live_location_duration_picker_title">"Wie lange soll der Live-Standort geteilt werden?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_share_location_live_location_disclaimer_title">"您的即時位置歷史將儲存於聊天室中,並在工作階段結束後對其他成員可見。"</string>
|
<string name="screen_share_location_live_location_disclaimer_title">"您的即時位置歷史將儲存於聊天室中,並在工作階段結束後對其他成員可見。"</string>
|
||||||
<string name="screen_share_location_live_location_duration_picker_title">"選擇分享即時位置的時間長度。"</string>
|
<string name="screen_share_location_live_location_duration_picker_title">"選擇分享即時位置的時間長度。"</string>
|
||||||
|
<string name="screen_share_location_live_location_missing_permissions">"您無權在此聊天室分享您的即時位置"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.share
|
|||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
||||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||||
@@ -58,6 +59,7 @@ class DefaultShareLocationEntryPointTest {
|
|||||||
sessionId = room.sessionId,
|
sessionId = room.sessionId,
|
||||||
),
|
),
|
||||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||||
|
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user