mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 12:54:35 +00:00
Merge branch 'develop' into fix/2614-linkify-ipv6-urls
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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,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)
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: 029db99513...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,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>
|
||||||
|
|||||||
+7
-1
@@ -10,6 +10,7 @@ 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
|
||||||
@@ -17,7 +18,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface EnterpriseService {
|
interface EnterpriseService {
|
||||||
val isEnterpriseBuild: Boolean
|
val isEnterpriseBuild: Boolean
|
||||||
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
|
||||||
suspend fun tweakMasUrl(url: String, homeserver: String): String
|
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
|
||||||
fun defaultHomeserverList(): List<String>
|
fun defaultHomeserverList(): List<String>
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||||
|
|
||||||
@@ -47,6 +48,11 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
fun overriddenElementWellKnown(): ElementWellKnown?
|
fun overriddenElementWellKnown(): ElementWellKnown?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
|
||||||
|
*/
|
||||||
|
fun essConfigEndpointUrl(domain: String): String?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.enterprise.api.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typealias for the well-known configuration used for enterprise features.
|
||||||
|
*
|
||||||
|
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
|
||||||
|
*/
|
||||||
|
typealias RemoteEnterpriseConfig = ElementWellKnown
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.enterprise.api.remoteconfig
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
|
||||||
|
*/
|
||||||
|
interface RemoteEnterpriseConfigProvider {
|
||||||
|
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
+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
|
||||||
|
}
|
||||||
+126
-4
@@ -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(
|
||||||
|
modifier = modifier
|
||||||
|
.then(testTag)
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
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,12 +376,70 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+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.")
|
||||||
|
|||||||
+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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -29,14 +29,11 @@ internal class MapTilerTileServerStyleUriBuilder(
|
|||||||
customMapStyleUrl: String?,
|
customMapStyleUrl: String?,
|
||||||
darkMode: Boolean,
|
darkMode: Boolean,
|
||||||
): String {
|
): String {
|
||||||
return buildString {
|
return if (customMapStyleUrl.isNullOrBlank()) {
|
||||||
if (customMapStyleUrl.isNullOrBlank()) {
|
|
||||||
val mapId = if (darkMode) darkMapId else lightMapId
|
val mapId = if (darkMode) darkMapId else lightMapId
|
||||||
append("$baseUrl/$mapId/style.json")
|
"$baseUrl/$mapId/style.json?key=$apiKey"
|
||||||
} else {
|
} else {
|
||||||
append(customMapStyleUrl)
|
customMapStyleUrl
|
||||||
}
|
|
||||||
append("?key=$apiKey")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-2
@@ -46,7 +46,7 @@ class MapTilerTileServerStyleUriBuilderTest {
|
|||||||
customMapStyleUrl = "https://custom.url/style.json",
|
customMapStyleUrl = "https://custom.url/style.json",
|
||||||
darkMode = false,
|
darkMode = false,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://custom.url/style.json?key=anApiKey")
|
).isEqualTo("https://custom.url/style.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -56,6 +56,36 @@ class MapTilerTileServerStyleUriBuilderTest {
|
|||||||
customMapStyleUrl = "https://custom.url/style.json",
|
customMapStyleUrl = "https://custom.url/style.json",
|
||||||
darkMode = true,
|
darkMode = true,
|
||||||
)
|
)
|
||||||
).isEqualTo("https://custom.url/style.json?key=anApiKey")
|
).isEqualTo("https://custom.url/style.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `custom map uri with its own api key is not altered`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
customMapStyleUrl = "https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey",
|
||||||
|
darkMode = false,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `custom map uri with an existing query is not altered`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
customMapStyleUrl = "https://self.hosted/style.json?foo=bar",
|
||||||
|
darkMode = true,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://self.hosted/style.json?foo=bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank custom map uri falls back to the built-in style`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
customMapStyleUrl = " ",
|
||||||
|
darkMode = false,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+5
-2
@@ -59,7 +59,7 @@ class DefaultBiometricAuthentication(
|
|||||||
private var cryptoObject: CryptoObject? = null
|
private var cryptoObject: CryptoObject? = null
|
||||||
|
|
||||||
override suspend fun setup() {
|
override suspend fun setup() {
|
||||||
if (cryptoObject != null) return
|
Timber.d("Setup biometric authentication")
|
||||||
try {
|
try {
|
||||||
val secretKey = ensureKey()
|
val secretKey = ensureKey()
|
||||||
val cipher = encryptionDecryptionService.createEncryptionCipher(secretKey)
|
val cipher = encryptionDecryptionService.createEncryptionCipher(secretKey)
|
||||||
@@ -126,6 +126,9 @@ private class AuthenticationCallback(
|
|||||||
if (this == null) return false
|
if (this == null) return false
|
||||||
return runCatchingExceptions {
|
return runCatchingExceptions {
|
||||||
doFinal("biometric_challenge".toByteArray())
|
doFinal("biometric_challenge".toByteArray())
|
||||||
}.isSuccess
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to validate cipher")
|
||||||
|
}
|
||||||
|
.isSuccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -13,13 +13,15 @@ import dev.zacsweers.metro.ContributesBinding
|
|||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
|
||||||
|
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAccountProviderAccessControl(
|
class DefaultAccountProviderAccessControl(
|
||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
private val wellknownRetriever: WellknownRetriever,
|
private val wellknownRetrieverFactory: WellknownRetriever.Factory,
|
||||||
|
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
|
||||||
) : AccountProviderAccessControl {
|
) : AccountProviderAccessControl {
|
||||||
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
||||||
assertIsAllowedToConnectToAccountProvider(
|
assertIsAllowedToConnectToAccountProvider(
|
||||||
@@ -38,9 +40,15 @@ class DefaultAccountProviderAccessControl(
|
|||||||
) {
|
) {
|
||||||
if (enterpriseService.isEnterpriseBuild.not()) {
|
if (enterpriseService.isEnterpriseBuild.not()) {
|
||||||
// Ensure that Element Pro is not required for this account provider
|
// Ensure that Element Pro is not required for this account provider
|
||||||
|
val temporaryMatrixClient = temporaryMatrixClientFactory.create(accountProviderUrl).getOrThrow()
|
||||||
|
temporaryMatrixClient.use {
|
||||||
|
val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
|
||||||
val wellKnown = wellknownRetriever.getElementWellKnown(
|
val wellKnown = wellknownRetriever.getElementWellKnown(
|
||||||
baseUrl = accountProviderUrl.ensureProtocol(),
|
host = accountProviderUrl,
|
||||||
|
// It's expected this is hardcoded to the well-known endpoint
|
||||||
|
source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT,
|
||||||
).dataOrNull()
|
).dataOrNull()
|
||||||
|
|
||||||
if (wellKnown?.enforceElementPro == true) {
|
if (wellKnown?.enforceElementPro == true) {
|
||||||
throw AccountProviderAccessException.NeedElementProException(
|
throw AccountProviderAccessException.NeedElementProException(
|
||||||
unauthorisedAccountProviderTitle = title,
|
unauthorisedAccountProviderTitle = title,
|
||||||
@@ -48,6 +56,7 @@ class DefaultAccountProviderAccessControl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
|
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
|
||||||
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
|
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
|
||||||
unauthorisedAccountProviderTitle = title,
|
unauthorisedAccountProviderTitle = title,
|
||||||
|
|||||||
+9
-2
@@ -14,6 +14,8 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
|||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -34,8 +36,13 @@ class HomeserverResolver(
|
|||||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||||
val flowContext = currentCoroutineContext()
|
val flowContext = currentCoroutineContext()
|
||||||
val trimmedUserInput = userInput.trim()
|
val trimmedUserInput = userInput.trim()
|
||||||
if (trimmedUserInput.length < 4) return@flow
|
val searchTerm = when {
|
||||||
val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/")
|
MatrixPatterns.isUserId(trimmedUserInput) -> UserId(trimmedUserInput).domainName.orEmpty()
|
||||||
|
trimmedUserInput.startsWith("@") -> return@flow
|
||||||
|
else -> trimmedUserInput
|
||||||
|
}
|
||||||
|
if (searchTerm.length < 4) return@flow
|
||||||
|
val candidateBase = searchTerm.ensureProtocol().removeSuffix("/")
|
||||||
val list = getUrlCandidates(candidateBase)
|
val list = getUrlCandidates(candidateBase)
|
||||||
val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
|
val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
|
||||||
// Run all the requests in parallel
|
// Run all the requests in parallel
|
||||||
|
|||||||
+14
-4
@@ -13,6 +13,8 @@ import dev.zacsweers.metro.AppScope
|
|||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
|
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
|
||||||
|
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -22,17 +24,24 @@ interface WebClientUrlForAuthenticationRetriever {
|
|||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultWebClientUrlForAuthenticationRetriever(
|
class DefaultWebClientUrlForAuthenticationRetriever(
|
||||||
private val wellknownRetriever: WellknownRetriever,
|
private val wellknownRetrieverFactory: WellknownRetriever.Factory,
|
||||||
|
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
|
||||||
) : WebClientUrlForAuthenticationRetriever {
|
) : WebClientUrlForAuthenticationRetriever {
|
||||||
override suspend fun retrieve(homeServerUrl: String): String {
|
override suspend fun retrieve(homeServerUrl: String): String {
|
||||||
if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) {
|
if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) {
|
||||||
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
||||||
throw AccountCreationNotSupported()
|
throw AccountCreationNotSupported()
|
||||||
}
|
}
|
||||||
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl).dataOrNull()
|
val temporaryMatrixClient = temporaryMatrixClientFactory.create(homeServerUrl).getOrThrow()
|
||||||
?: throw AccountCreationNotSupported()
|
return temporaryMatrixClient.use {
|
||||||
|
val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
|
||||||
|
val wellknown = wellknownRetriever.getElementWellKnown(
|
||||||
|
host = homeServerUrl,
|
||||||
|
source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT
|
||||||
|
).dataOrNull() ?: throw AccountCreationNotSupported()
|
||||||
|
|
||||||
val registrationHelperUrl = wellknown.registrationHelperUrl
|
val registrationHelperUrl = wellknown.registrationHelperUrl
|
||||||
return if (registrationHelperUrl != null) {
|
if (registrationHelperUrl != null) {
|
||||||
registrationHelperUrl.toUri()
|
registrationHelperUrl.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("hs_url", homeServerUrl)
|
.appendQueryParameter("hs_url", homeServerUrl)
|
||||||
@@ -42,4 +51,5 @@ class DefaultWebClientUrlForAuthenticationRetriever(
|
|||||||
throw AccountCreationNotSupported()
|
throw AccountCreationNotSupported()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,14 @@
|
|||||||
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
|
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
|
||||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_open_element_classic">"Element Classic öffnen"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_1">"Öffne Element Classic auf deinem Gerät"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_2_android">"Geh zu Einstellungen > Sicherheit & Datenschutz"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_4">"Folge den Anweisungen, um den Schlüsselspeicher zu aktivieren."</string>
|
||||||
|
<string name="screen_missing_key_backup_step_5">"Zurück zu %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_title">"Aktiviere deinen Schlüsselspeicher, bevor du mit %1$s fortfährst."</string>
|
||||||
<string name="screen_onboarding_app_version">"Version %1$s"</string>
|
<string name="screen_onboarding_app_version">"Version %1$s"</string>
|
||||||
|
<string name="screen_onboarding_checking_account">"Konto wird geprüft"</string>
|
||||||
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
|
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
|
||||||
<string name="screen_onboarding_sign_in_to">"Anmelden bei %1$s"</string>
|
<string name="screen_onboarding_sign_in_to">"Anmelden bei %1$s"</string>
|
||||||
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
|
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<string name="screen_change_server_subtitle">"Vad är adressen till din server?"</string>
|
<string name="screen_change_server_subtitle">"Vad är adressen till din server?"</string>
|
||||||
<string name="screen_change_server_title">"Välj din server"</string>
|
<string name="screen_change_server_title">"Välj din server"</string>
|
||||||
<string name="screen_create_account_title">"Skapa konto"</string>
|
<string name="screen_create_account_title">"Skapa konto"</string>
|
||||||
<string name="screen_login_error_deactivated_account">"Detta konto har avaktiverats."</string>
|
<string name="screen_login_error_deactivated_account">"Detta konto har raderats."</string>
|
||||||
<string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string>
|
<string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string>
|
||||||
<string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string>
|
<string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string>
|
||||||
<string name="screen_login_error_refresh_tokens">"Den här servern är konfigurerad för att använda uppdateringstokens. Dessa stöds inte när du använder lösenordsbaserad inloggning."</string>
|
<string name="screen_login_error_refresh_tokens">"Den här servern är konfigurerad för att använda uppdateringstokens. Dessa stöds inte när du använder lösenordsbaserad inloggning."</string>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
|
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org 由 Matrix.org 基金會營運,是用於安全、去中心化通訊的公共 Matrix 網路上的大型免費伺服器。"</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org 由 Matrix.org 基金會營運,是用於安全、去中心化通訊的公共 Matrix 網路上的大型免費伺服器。"</string>
|
||||||
<string name="screen_change_account_provider_other">"其他"</string>
|
<string name="screen_change_account_provider_other">"其他"</string>
|
||||||
<string name="screen_change_account_provider_subtitle">"使用不同的帳戶提供者,例如您自己的伺服器或工作帳號。"</string>
|
<string name="screen_change_account_provider_subtitle">"使用不同的帳號提供者,例如您自己的伺服器或工作帳號。"</string>
|
||||||
<string name="screen_change_account_provider_title">"更改帳號提供者"</string>
|
<string name="screen_change_account_provider_title">"更改帳號提供者"</string>
|
||||||
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
|
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
|
||||||
<string name="screen_change_server_error_element_pro_required_message">"%1$s 需要 Element Pro 應用程式。請從商店下載。"</string>
|
<string name="screen_change_server_error_element_pro_required_message">"%1$s 需要 Element Pro 應用程式。請從商店下載。"</string>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<string name="screen_change_server_subtitle">"您的伺服器地址?"</string>
|
<string name="screen_change_server_subtitle">"您的伺服器地址?"</string>
|
||||||
<string name="screen_change_server_title">"選擇您的伺服器"</string>
|
<string name="screen_change_server_title">"選擇您的伺服器"</string>
|
||||||
<string name="screen_create_account_title">"建立帳號"</string>
|
<string name="screen_create_account_title">"建立帳號"</string>
|
||||||
<string name="screen_login_error_deactivated_account">"這個帳號已被停用。"</string>
|
<string name="screen_login_error_deactivated_account">"此帳號已被刪除。"</string>
|
||||||
<string name="screen_login_error_invalid_credentials">"不正確的使用者名稱或密碼"</string>
|
<string name="screen_login_error_invalid_credentials">"不正確的使用者名稱或密碼"</string>
|
||||||
<string name="screen_login_error_invalid_user_id">"此非有效的使用者識別字串。預期的格式:‘@user:homeserver.org’"</string>
|
<string name="screen_login_error_invalid_user_id">"此非有效的使用者識別字串。預期的格式:‘@user:homeserver.org’"</string>
|
||||||
<string name="screen_login_error_refresh_tokens">"此伺服器已設定為使用重新整理權杖。使用以密碼為基礎的登入方式時,不支援這些功能。"</string>
|
<string name="screen_login_error_refresh_tokens">"此伺服器已設定為使用重新整理權杖。使用以密碼為基礎的登入方式時,不支援這些功能。"</string>
|
||||||
|
|||||||
+6
-1
@@ -12,10 +12,12 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
||||||
|
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -156,8 +158,9 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
||||||
defaultHomeserverListResult = { allowedAccountProviders },
|
defaultHomeserverListResult = { allowedAccountProviders },
|
||||||
),
|
),
|
||||||
|
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(
|
||||||
wellknownRetriever = FakeWellknownRetriever(
|
wellknownRetriever = FakeWellknownRetriever(
|
||||||
getElementWellKnownResult = {
|
getElementWellKnownResult = { _, _ ->
|
||||||
if (elementWellKnown == null) {
|
if (elementWellKnown == null) {
|
||||||
WellknownRetrieverResult.NotFound
|
WellknownRetrieverResult.NotFound
|
||||||
} else {
|
} else {
|
||||||
@@ -165,6 +168,8 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
|
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
|
||||||
|
|||||||
+8
-9
@@ -17,18 +17,19 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
|
|||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
|
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
@@ -158,7 +159,8 @@ class ChangeServerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change server element pro required error`() = runTest {
|
fun `present - change server element pro required error`() = runTest {
|
||||||
val getElementWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<ElementWellKnown>> {
|
val getElementWellKnownResult = lambdaRecorder { homeserver: String, _: EnterpriseRemoteConfigSource ->
|
||||||
|
assertThat(homeserver).isEqualTo(A_HOMESERVER_URL)
|
||||||
WellknownRetrieverResult.Success(
|
WellknownRetrieverResult.Success(
|
||||||
anElementWellKnown(
|
anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
@@ -174,8 +176,6 @@ class ChangeServerPresenterTest {
|
|||||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
|
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
|
||||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
|
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
|
||||||
@@ -183,9 +183,7 @@ class ChangeServerPresenterTest {
|
|||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
||||||
).isEqualTo("io.element.enterprise")
|
).isEqualTo("io.element.enterprise")
|
||||||
getElementWellKnownResult.assertions()
|
getElementWellKnownResult.assertions().isCalledOnce()
|
||||||
.isCalledOnce()
|
|
||||||
.with(value(A_HOMESERVER_URL.ensureProtocol()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +267,8 @@ class ChangeServerPresenterTest {
|
|||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetriever = wellknownRetriever,
|
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
||||||
|
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||||
),
|
),
|
||||||
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||||
advisor = localNetworkPermissionAdvisor,
|
advisor = localNetworkPermissionAdvisor,
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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.login.impl.resolver
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HomeserverResolverTest {
|
||||||
|
@Test
|
||||||
|
fun `resolve - a server name is used as is`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - an url is used as is`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("https://example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id resolves the server name of the user id`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id with a port resolves the server name of the user id`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org:8448") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:example.org:8448").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org:8448")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org:8448")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - an incomplete user id does not resolve anything`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(true) }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
sut.resolve("@alice:").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id with a too short server name does not resolve anything`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(true) }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:a.b").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createHomeserverResolver(
|
||||||
|
checkResult: (String) -> Result<Boolean>,
|
||||||
|
) = HomeserverResolver(
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
)
|
||||||
|
}
|
||||||
+10
-4
@@ -20,6 +20,7 @@ import io.element.android.features.login.impl.login.LoginModePresenter
|
|||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
@@ -30,11 +31,15 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||||
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||||
|
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
@@ -304,7 +309,8 @@ private fun createPresenter(
|
|||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetriever = wellknownRetriever,
|
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
||||||
|
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||||
),
|
),
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginModePresenter = loginModePresenter,
|
loginModePresenter = loginModePresenter,
|
||||||
@@ -318,9 +324,9 @@ fun createLoginModePresenter(
|
|||||||
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||||
localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor =
|
localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor =
|
||||||
io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(),
|
FakeLocalNetworkPermissionAdvisor(),
|
||||||
permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory =
|
permissionsPresenterFactory: PermissionsPresenter.Factory =
|
||||||
io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(),
|
FakePermissionsPresenterFactory(),
|
||||||
): LoginModePresenter = LoginModePresenter(
|
): LoginModePresenter = LoginModePresenter(
|
||||||
oAuthActionFlow = oAuthActionFlow,
|
oAuthActionFlow = oAuthActionFlow,
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.pinned
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
|
internal fun List<MatrixTimelineItem>.keepDisplayablePinnedEvents(): List<MatrixTimelineItem> {
|
||||||
|
val items = filter { it.isDisplayableAsPinnedEvent() }
|
||||||
|
return items.filterIndexed { index, item ->
|
||||||
|
!item.isDayDivider() || items.getOrNull(index + 1) is MatrixTimelineItem.Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun EventContent.isDisplayableAsPinnedEvent(): Boolean {
|
||||||
|
return when (this) {
|
||||||
|
UnknownContent,
|
||||||
|
is FailedToParseMessageLikeContent,
|
||||||
|
is FailedToParseStateContent -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MatrixTimelineItem.isDisplayableAsPinnedEvent(): Boolean {
|
||||||
|
return this !is MatrixTimelineItem.Event || event.content.isDisplayableAsPinnedEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MatrixTimelineItem.isDayDivider(): Boolean {
|
||||||
|
return this is MatrixTimelineItem.Virtual && virtual is VirtualTimelineItem.DayDivider
|
||||||
|
}
|
||||||
+2
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.pinned.banner
|
|||||||
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.messages.impl.pinned.isDisplayableAsPinnedEvent
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
@@ -24,6 +25,7 @@ class PinnedMessagesBannerItemFactory(
|
|||||||
when (timelineItem) {
|
when (timelineItem) {
|
||||||
is MatrixTimelineItem.Event -> {
|
is MatrixTimelineItem.Event -> {
|
||||||
val eventId = timelineItem.eventId ?: return@withContext null
|
val eventId = timelineItem.eventId ?: return@withContext null
|
||||||
|
if (!timelineItem.event.content.isDisplayableAsPinnedEvent()) return@withContext null
|
||||||
val formatted = formatter.format(timelineItem.event)
|
val formatted = formatter.format(timelineItem.event)
|
||||||
PinnedMessagesBannerItem(
|
PinnedMessagesBannerItem(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
|
|||||||
+3
-2
@@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
|
|||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.link.LinkState
|
import io.element.android.features.messages.impl.link.LinkState
|
||||||
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||||
|
import io.element.android.features.messages.impl.pinned.keepDisplayablePinnedEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||||
@@ -207,7 +208,7 @@ class PinnedMessagesListPresenter(
|
|||||||
val timelineItemsFlow = asyncTimeline.data.timelineItems
|
val timelineItemsFlow = asyncTimeline.data.timelineItems
|
||||||
combine(timelineItemsFlow, room.membersStateFlow) { items, membersState ->
|
combine(timelineItemsFlow, room.membersStateFlow) { items, membersState ->
|
||||||
timelineItemsFactory.replaceWith(
|
timelineItemsFactory.replaceWith(
|
||||||
timelineItems = items,
|
timelineItems = items.keepDisplayablePinnedEvents(),
|
||||||
roomMembers = membersState.roomMembers().orEmpty(),
|
roomMembers = membersState.roomMembers().orEmpty(),
|
||||||
renderReadReceipts = false,
|
renderReadReceipts = false,
|
||||||
)
|
)
|
||||||
@@ -239,7 +240,7 @@ class PinnedMessagesListPresenter(
|
|||||||
AsyncData.Uninitialized, is AsyncData.Loading -> PinnedMessagesListState.Loading
|
AsyncData.Uninitialized, is AsyncData.Loading -> PinnedMessagesListState.Loading
|
||||||
is AsyncData.Failure -> PinnedMessagesListState.Failed
|
is AsyncData.Failure -> PinnedMessagesListState.Failed
|
||||||
is AsyncData.Success -> {
|
is AsyncData.Success -> {
|
||||||
if (timelineItems.data.isEmpty()) {
|
if (timelineItems.data.all { it is TimelineItem.Virtual }) {
|
||||||
PinnedMessagesListState.Empty
|
PinnedMessagesListState.Empty
|
||||||
} else {
|
} else {
|
||||||
val actionListState = actionListPresenter.present()
|
val actionListState = actionListPresenter.present()
|
||||||
|
|||||||
+13
-1
@@ -28,6 +28,7 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.messages.impl.MessagesEvent
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.MessagesPresenter
|
import io.element.android.features.messages.impl.MessagesPresenter
|
||||||
import io.element.android.features.messages.impl.MessagesState
|
import io.element.android.features.messages.impl.MessagesState
|
||||||
@@ -40,6 +41,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
@@ -54,6 +56,7 @@ 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.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -91,6 +94,7 @@ class ThreadedMessagesNode(
|
|||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||||
|
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val threadRootEventId: ThreadId,
|
val threadRootEventId: ThreadId,
|
||||||
@@ -322,7 +326,15 @@ class ThreadedMessagesNode(
|
|||||||
onViewAllPinnedMessagesClick = {},
|
onViewAllPinnedMessagesClick = {},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
knockRequestsBannerView = {},
|
knockRequestsBannerView = {},
|
||||||
customReactionBottomSheet = {},
|
customReactionBottomSheet = {
|
||||||
|
CustomReactionBottomSheet(
|
||||||
|
state = state.customReactionState,
|
||||||
|
onSelectEmoji = { uniqueId, emoji ->
|
||||||
|
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
|
||||||
|
},
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
|
)
|
||||||
|
},
|
||||||
onThreadsListClick = {},
|
onThreadsListClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+7
-10
@@ -125,6 +125,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
|||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.mediaSources
|
import io.element.android.libraries.matrix.api.timeline.item.event.mediaSources
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.toMatrixUser
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
|
||||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState
|
||||||
@@ -207,11 +208,7 @@ fun TimelineItemEventRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onUserDataClick() {
|
fun onUserDataClick() {
|
||||||
val sender = MatrixUser(
|
val sender = event.senderProfile.toMatrixUser(event.senderId)
|
||||||
userId = event.senderId,
|
|
||||||
displayName = event.senderProfile.getDisplayName(),
|
|
||||||
avatarUrl = event.senderProfile.getAvatarUrl(),
|
|
||||||
)
|
|
||||||
onUserDataClick(sender)
|
onUserDataClick(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,15 +801,15 @@ private fun MessageEventBubbleContent(
|
|||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
|
|
||||||
val talkbackCompatModifier = if (isTalkbackActive()) {
|
val talkbackCompatModifier = when {
|
||||||
// Use z-index to make the replied to text being read after the message
|
// Use z-index to make the replied to text being read after the message
|
||||||
// Usually, you'd use traversalIndex for that, but it's not working for some reason
|
// Usually, you'd use traversalIndex for that, but it's not working for some reason
|
||||||
inReplyToModifier.zIndex(1f)
|
isTalkbackActive() -> inReplyToModifier.zIndex(1f)
|
||||||
} else {
|
inReplyTo is InReplyToDetails.Error -> inReplyToModifier
|
||||||
inReplyToModifier.clickable(onClick = inReplyToClick)
|
else -> inReplyToModifier.clickable(onClick = inReplyToClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentHasError = currentContentValidationState.hasError()
|
val contentHasError = currentContentValidationState.hasError() || inReplyTo is InReplyToDetails.Error
|
||||||
val borderColor = if (contentHasError) ElementTheme.colors.borderCriticalSubtle else ElementTheme.colors.separatorPrimary
|
val borderColor = if (contentHasError) ElementTheme.colors.borderCriticalSubtle else ElementTheme.colors.separatorPrimary
|
||||||
val backgroundColor = if (contentHasError) ElementTheme.colors.bgCriticalSubtle else ElementTheme.colors.bgCanvasDefault
|
val backgroundColor = if (contentHasError) ElementTheme.colors.bgCriticalSubtle else ElementTheme.colors.bgCanvasDefault
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
+17
@@ -8,11 +8,16 @@
|
|||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.isImeVisible
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.SheetValue
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.rememberBottomSheetState
|
import androidx.compose.material3.rememberBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
@@ -54,6 +59,7 @@ fun CustomReactionBottomSheet(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
) {
|
) {
|
||||||
|
ExpandSheetWhenImeIsVisible(sheetState)
|
||||||
emojiPickerRenderer.Render(
|
emojiPickerRenderer.Render(
|
||||||
state = state.target.emojiPickerState,
|
state = state.target.emojiPickerState,
|
||||||
onSelectEmoji = ::onEmojiSelectedDismiss,
|
onSelectEmoji = ::onEmojiSelectedDismiss,
|
||||||
@@ -67,3 +73,14 @@ fun CustomReactionBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun ExpandSheetWhenImeIsVisible(sheetState: SheetState) {
|
||||||
|
val isImeVisible = WindowInsets.isImeVisible
|
||||||
|
LaunchedEffect(isImeVisible) {
|
||||||
|
if (isImeVisible) {
|
||||||
|
sheetState.expand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -58,10 +58,12 @@ fun TimelineItemEventContent.canBeForwarded(): Boolean =
|
|||||||
is TimelineItemFileContent,
|
is TimelineItemFileContent,
|
||||||
is TimelineItemAudioContent,
|
is TimelineItemAudioContent,
|
||||||
is TimelineItemVideoContent,
|
is TimelineItemVideoContent,
|
||||||
is TimelineItemLocationContent,
|
|
||||||
is TimelineItemVoiceContent,
|
is TimelineItemVoiceContent,
|
||||||
is TimelineItemGalleryContent,
|
is TimelineItemGalleryContent,
|
||||||
is TimelineItemAttachmentsContent -> true
|
is TimelineItemAttachmentsContent -> true
|
||||||
|
// Live location shares can't be forwarded, the SDK rejects them, so we only show the option for static locations
|
||||||
|
// See https://github.com/element-hq/element-x-android/issues/7190
|
||||||
|
is TimelineItemLocationContent -> mode is TimelineItemLocationContent.Mode.Static
|
||||||
// Stickers can't be forwarded (yet) so we don't show the option
|
// Stickers can't be forwarded (yet) so we don't show the option
|
||||||
// See https://github.com/element-hq/element-x-android/issues/2161
|
// See https://github.com/element-hq/element-x-android/issues/2161
|
||||||
is TimelineItemStickerContent -> false
|
is TimelineItemStickerContent -> false
|
||||||
|
|||||||
+3
-6
@@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateSetOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
@@ -20,7 +19,6 @@ import io.element.android.features.contentscanner.api.ContentScannerService
|
|||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.mapState
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
||||||
import io.element.android.libraries.matrix.api.media.isPreviewEnabled
|
import io.element.android.libraries.matrix.api.media.isPreviewEnabled
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
@@ -32,9 +30,8 @@ class TimelineProtectionPresenter(
|
|||||||
private val mediaPreviewService: MediaPreviewService,
|
private val mediaPreviewService: MediaPreviewService,
|
||||||
private val room: BaseRoom,
|
private val room: BaseRoom,
|
||||||
private val contentScannerService: ContentScannerService,
|
private val contentScannerService: ContentScannerService,
|
||||||
|
private val timelineProtectionStore: TimelineProtectionStore,
|
||||||
) : Presenter<TimelineProtectionState> {
|
) : Presenter<TimelineProtectionState> {
|
||||||
private val allowedEvents = mutableStateSetOf<EventId>()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineProtectionState {
|
override fun present(): TimelineProtectionState {
|
||||||
val mediaPreviewValue = remember {
|
val mediaPreviewValue = remember {
|
||||||
@@ -48,7 +45,7 @@ class TimelineProtectionPresenter(
|
|||||||
if (isPreviewEnabled) {
|
if (isPreviewEnabled) {
|
||||||
ProtectionState.RenderAll
|
ProtectionState.RenderAll
|
||||||
} else {
|
} else {
|
||||||
ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
|
ProtectionState.RenderOnly(eventIds = timelineProtectionStore.allowedEventIds.toImmutableSet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +53,7 @@ class TimelineProtectionPresenter(
|
|||||||
fun handleEvent(event: TimelineProtectionEvent) {
|
fun handleEvent(event: TimelineProtectionEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is TimelineProtectionEvent.ShowContent -> {
|
is TimelineProtectionEvent.ShowContent -> {
|
||||||
allowedEvents += setOfNotNull(event.eventId)
|
event.eventId?.let(timelineProtectionStore::allowEvent)
|
||||||
}
|
}
|
||||||
is TimelineProtectionEvent.ValidateContent -> {
|
is TimelineProtectionEvent.ValidateContent -> {
|
||||||
contentScannerService.scan(event.mediaSources, event.validationState)
|
contentScannerService.scan(event.mediaSources, event.validationState)
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.messages.impl.timeline.protection
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateSetOf
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateSet
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
interface TimelineProtectionStore {
|
||||||
|
val allowedEventIds: SnapshotStateSet<EventId>
|
||||||
|
fun allowEvent(eventId: EventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
@SingleIn(SessionScope::class)
|
||||||
|
class DefaultTimelineProtectionStore : TimelineProtectionStore {
|
||||||
|
private val allowedEvents = mutableStateSetOf<EventId>()
|
||||||
|
|
||||||
|
override val allowedEventIds: SnapshotStateSet<EventId> = allowedEvents
|
||||||
|
|
||||||
|
override fun allowEvent(eventId: EventId) {
|
||||||
|
allowedEvents += eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,10 @@
|
|||||||
<string name="screen_room_timeline_reactions_show_more">"Több megjelenítése"</string>
|
<string name="screen_room_timeline_reactions_show_more">"Több megjelenítése"</string>
|
||||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Reakció-összefoglaló megjelenítése"</string>
|
<string name="screen_room_timeline_reactions_show_reactions_summary">"Reakció-összefoglaló megjelenítése"</string>
|
||||||
<string name="screen_room_timeline_read_marker_title">"Új"</string>
|
<string name="screen_room_timeline_read_marker_title">"Új"</string>
|
||||||
|
<plurals name="screen_room_timeline_redacted_messages">
|
||||||
|
<item quantity="one">"%1$d eltávolított üzenet"</item>
|
||||||
|
<item quantity="other">"%1$d eltávolított üzenet"</item>
|
||||||
|
</plurals>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="one">"%1$d szobaváltozás"</item>
|
<item quantity="one">"%1$d szobaváltozás"</item>
|
||||||
<item quantity="other">"%1$d szobaváltozás"</item>
|
<item quantity="other">"%1$d szobaváltozás"</item>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<string name="screen_media_upload_preview_processing">"Przetwarzanie…"</string>
|
<string name="screen_media_upload_preview_processing">"Przetwarzanie…"</string>
|
||||||
<string name="screen_media_upload_preview_title">"Dodaj media"</string>
|
<string name="screen_media_upload_preview_title">"Dodaj media"</string>
|
||||||
<string name="screen_report_content_block_user">"Zablokuj użytkownika"</string>
|
<string name="screen_report_content_block_user">"Zablokuj użytkownika"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Sprawdź, czy chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika."</string>
|
<string name="screen_report_content_block_user_hint">"Zaznacz, jeśli chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika."</string>
|
||||||
<string name="screen_report_content_explanation">"Ta wiadomość zostanie zgłoszona do administratora Twojego serwera domowego. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości."</string>
|
<string name="screen_report_content_explanation">"Ta wiadomość zostanie zgłoszona do administratora Twojego serwera domowego. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości."</string>
|
||||||
<string name="screen_report_content_hint">"Powód zgłoszenia treści"</string>
|
<string name="screen_report_content_hint">"Powód zgłoszenia treści"</string>
|
||||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||||
|
|||||||
@@ -7,6 +7,19 @@
|
|||||||
<string name="crypto_event_authenticity_unknown_device">"Criptat de un dispozitiv necunoscut sau șters."</string>
|
<string name="crypto_event_authenticity_unknown_device">"Criptat de un dispozitiv necunoscut sau șters."</string>
|
||||||
<string name="crypto_event_authenticity_unsigned_device">"Criptat de un dispozitiv care nu este verificat de proprietarul său."</string>
|
<string name="crypto_event_authenticity_unsigned_device">"Criptat de un dispozitiv care nu este verificat de proprietarul său."</string>
|
||||||
<string name="crypto_event_authenticity_unverified_identity">"Criptat de un utilizator neverificat."</string>
|
<string name="crypto_event_authenticity_unverified_identity">"Criptat de un utilizator neverificat."</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"Răsturnați imaginea pe orizontală"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Răsturnat pe orizontală"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"Răsturnați imaginea pe verticală"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Răsturnat pe verticală"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Original"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rotiți imaginea la stânga"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="one">"%1$d grad"</item>
|
||||||
|
<item quantity="few">"%1$d grade"</item>
|
||||||
|
<item quantity="other">"%1$d grade"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"Editați poza"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi."</string>
|
<string name="screen_media_upload_preview_caption_warning">"Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi."</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Atingeți pentru a modifica calitatea încărcării videoclipului"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"Atingeți pentru a modifica calitatea încărcării videoclipului"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Fișierul nu a putut fi încărcat."</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Fișierul nu a putut fi încărcat."</string>
|
||||||
@@ -17,6 +30,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"Elementul %1$d din %2$d"</string>
|
<string name="screen_media_upload_preview_item_count">"Elementul %1$d din %2$d"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimizați calitatea imaginii"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimizați calitatea imaginii"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"Se procesează…"</string>
|
<string name="screen_media_upload_preview_processing">"Se procesează…"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"Adăugați media"</string>
|
||||||
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
<string name="screen_report_content_block_user">"Blocați utilizatorul"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
|
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
|
||||||
<string name="screen_report_content_explanation">"Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat."</string>
|
<string name="screen_report_content_explanation">"Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat."</string>
|
||||||
@@ -59,6 +73,11 @@
|
|||||||
<string name="screen_room_timeline_reactions_show_more">"Afișați mai mult"</string>
|
<string name="screen_room_timeline_reactions_show_more">"Afișați mai mult"</string>
|
||||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Afișați rezumatul reacțiilor"</string>
|
<string name="screen_room_timeline_reactions_show_reactions_summary">"Afișați rezumatul reacțiilor"</string>
|
||||||
<string name="screen_room_timeline_read_marker_title">"Nou"</string>
|
<string name="screen_room_timeline_read_marker_title">"Nou"</string>
|
||||||
|
<plurals name="screen_room_timeline_redacted_messages">
|
||||||
|
<item quantity="one">"%1$d mesaj șters"</item>
|
||||||
|
<item quantity="few">"%1$d mesaje șterse"</item>
|
||||||
|
<item quantity="other">"%1$d mesaje șterse"</item>
|
||||||
|
</plurals>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="one">"%1$d schimbare a camerii"</item>
|
<item quantity="one">"%1$d schimbare a camerii"</item>
|
||||||
<item quantity="few">"%1$d schimbări ale camerei"</item>
|
<item quantity="few">"%1$d schimbări ale camerei"</item>
|
||||||
|
|||||||
@@ -7,6 +7,17 @@
|
|||||||
<string name="crypto_event_authenticity_unknown_device">"由未知或已刪除的裝置加密。"</string>
|
<string name="crypto_event_authenticity_unknown_device">"由未知或已刪除的裝置加密。"</string>
|
||||||
<string name="crypto_event_authenticity_unsigned_device">"由未經其擁有者驗證的裝置加密。"</string>
|
<string name="crypto_event_authenticity_unsigned_device">"由未經其擁有者驗證的裝置加密。"</string>
|
||||||
<string name="crypto_event_authenticity_unverified_identity">"由未經驗證的使用者加密。"</string>
|
<string name="crypto_event_authenticity_unverified_identity">"由未經驗證的使用者加密。"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻轉圖片"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻轉"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始狀態"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"垂直翻轉圖片"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"垂直翻轉"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"原始狀態"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"向左旋轉圖片"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="other">"%1$d 度"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"編輯照片"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
|
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
|
||||||
@@ -17,6 +28,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"第 %1$d 個項目,共 %2$d 個"</string>
|
<string name="screen_media_upload_preview_item_count">"第 %1$d 個項目,共 %2$d 個"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"最佳化影像品質"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"最佳化影像品質"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"正在處理……"</string>
|
<string name="screen_media_upload_preview_processing">"正在處理……"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"新增媒體"</string>
|
||||||
<string name="screen_report_content_block_user">"封鎖使用者"</string>
|
<string name="screen_report_content_block_user">"封鎖使用者"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"</string>
|
<string name="screen_report_content_block_user_hint">"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"</string>
|
||||||
<string name="screen_report_content_explanation">"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"</string>
|
<string name="screen_report_content_explanation">"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"</string>
|
||||||
@@ -55,6 +67,9 @@
|
|||||||
<string name="screen_room_timeline_reactions_show_more">"更多"</string>
|
<string name="screen_room_timeline_reactions_show_more">"更多"</string>
|
||||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"顯示反應摘要"</string>
|
<string name="screen_room_timeline_reactions_show_reactions_summary">"顯示反應摘要"</string>
|
||||||
<string name="screen_room_timeline_read_marker_title">"新訊息"</string>
|
<string name="screen_room_timeline_read_marker_title">"新訊息"</string>
|
||||||
|
<plurals name="screen_room_timeline_redacted_messages">
|
||||||
|
<item quantity="other">"%1$d 則已移除的訊息"</item>
|
||||||
|
</plurals>
|
||||||
<plurals name="screen_room_timeline_state_changes">
|
<plurals name="screen_room_timeline_state_changes">
|
||||||
<item quantity="other">"%1$d 個聊天室變更"</item>
|
<item quantity="other">"%1$d 個聊天室變更"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|||||||
+120
@@ -21,7 +21,10 @@ import io.element.android.features.messages.impl.timeline.model.event.RtcNotific
|
|||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aLiveLocationMode
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aStaticLocationMode
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemLocationContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
@@ -601,6 +604,123 @@ class ActionListPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for a static location item`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
isEditable = false,
|
||||||
|
content = aTimelineItemLocationContent(mode = aStaticLocationMode()),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvent.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
),
|
||||||
|
recentEmojis = suggestedEmojis,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvent.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for a live location item does not offer to forward it`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
isEditable = false,
|
||||||
|
content = aTimelineItemLocationContent(mode = aLiveLocationMode(isActive = true)),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvent.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
sentTimeFull = "0 Full true",
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
),
|
||||||
|
recentEmojis = suggestedEmojis,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvent.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for an ended live location item does not offer to forward it`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
isEditable = false,
|
||||||
|
content = aTimelineItemLocationContent(mode = aLiveLocationMode(isActive = false)),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvent.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat((successState.target as ActionListState.Target.Success).actions)
|
||||||
|
.doesNotContain(TimelineItemAction.Forward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for a media with caption item`() = runTest {
|
fun `present - compute for a media with caption item`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.messages.impl.pinned.banner
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PinnedMessagesBannerItemFactoryTest {
|
||||||
|
@Test
|
||||||
|
fun `create - returns an item for an event which can be displayed`() = runTest {
|
||||||
|
val factory = createPinnedMessagesBannerItemFactory()
|
||||||
|
val item = factory.create(anEventTimelineItemOf(aMessageContent("A message")))
|
||||||
|
assertThat(item?.eventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create - returns null for events which cannot be displayed`() = runTest {
|
||||||
|
val factory = createPinnedMessagesBannerItemFactory()
|
||||||
|
val contents = listOf(
|
||||||
|
UnknownContent,
|
||||||
|
FailedToParseMessageLikeContent(eventType = "m.room.message", error = "an error"),
|
||||||
|
FailedToParseStateContent(eventType = "m.room.topic", stateKey = "", error = "an error"),
|
||||||
|
)
|
||||||
|
for (content in contents) {
|
||||||
|
assertThat(factory.create(anEventTimelineItemOf(content))).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create - returns null for virtual items`() = runTest {
|
||||||
|
val factory = createPinnedMessagesBannerItemFactory()
|
||||||
|
val item = factory.create(
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = A_UNIQUE_ID,
|
||||||
|
virtual = VirtualTimelineItem.DayDivider(timestamp = 0L),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(item).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anEventTimelineItemOf(content: EventContent) = MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = content,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun TestScope.createPinnedMessagesBannerItemFactory() = PinnedMessagesBannerItemFactory(
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
formatter = FakePinnedMessagesBannerFormatter(
|
||||||
|
formatLambda = { event -> "${event.content}" }
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
+42
@@ -14,6 +14,7 @@ import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBanner
|
|||||||
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.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||||
@@ -24,6 +25,7 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
|||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@@ -90,6 +92,46 @@ class PinnedMessagesBannerPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - loaded state - events which cannot be displayed are not rendered`() = runTest {
|
||||||
|
val messageContent = aMessageContent("A message")
|
||||||
|
val pinnedEventsTimeline = FakeTimeline(
|
||||||
|
timelineItems = flowOf(
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = messageContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = UnknownContent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val room = FakeJoinedRoom(
|
||||||
|
createTimelineResult = { Result.success(pinnedEventsTimeline) }
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
|
||||||
|
}
|
||||||
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
|
state is PinnedMessagesBannerState.Loaded
|
||||||
|
}.last() as PinnedMessagesBannerState.Loaded
|
||||||
|
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1)
|
||||||
|
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0)
|
||||||
|
assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent.toString())
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - loaded state - multiple pinned messages`() = runTest {
|
fun `present - loaded state - multiple pinned messages`() = runTest {
|
||||||
val messageContent1 = aMessageContent("A message")
|
val messageContent1 = aMessageContent("A message")
|
||||||
|
|||||||
+97
-9
@@ -21,13 +21,19 @@ import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvi
|
|||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
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.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
@@ -38,6 +44,7 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
|||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
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.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
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
|
||||||
@@ -131,6 +138,90 @@ class PinnedMessagesListPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - filled state - events which cannot be displayed are not rendered`() = runTest {
|
||||||
|
val pinnedEventsTimeline = createPinnedMessagesTimeline(
|
||||||
|
items = listOf(
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = UniqueId("aDayDivider"),
|
||||||
|
virtual = VirtualTimelineItem.DayDivider(timestamp = 0L),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aMessageContent("A message"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = UniqueId("anotherDayDivider"),
|
||||||
|
virtual = VirtualTimelineItem.DayDivider(timestamp = 1L),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = UnknownContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val room = FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
roomPermissions = roomPermissions(),
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
|
||||||
|
},
|
||||||
|
createTimelineResult = { Result.success(pinnedEventsTimeline) },
|
||||||
|
)
|
||||||
|
val presenter = createPinnedMessagesListPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
val filledState = consumeItemsUntilPredicate { state ->
|
||||||
|
state is PinnedMessagesListState.Filled
|
||||||
|
}.last() as PinnedMessagesListState.Filled
|
||||||
|
assertThat(filledState.loadedPinnedMessagesCount).isEqualTo(1)
|
||||||
|
assertThat(filledState.timelineItems).hasSize(2)
|
||||||
|
assertThat(filledState.timelineItems.filterIsInstance<TimelineItem.Event>().map { it.eventId }).containsExactly(AN_EVENT_ID)
|
||||||
|
assertThat(filledState.timelineItems.filterIsInstance<TimelineItem.Virtual>()).hasSize(1)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - empty state when no pinned event can be displayed`() = runTest {
|
||||||
|
val pinnedEventsTimeline = createPinnedMessagesTimeline(
|
||||||
|
items = listOf(
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = A_UNIQUE_ID,
|
||||||
|
virtual = VirtualTimelineItem.DayDivider(timestamp = 0L),
|
||||||
|
),
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = A_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = FailedToParseMessageLikeContent(eventType = "m.room.message", error = "an error"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val room = FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
roomPermissions = roomPermissions(),
|
||||||
|
).apply {
|
||||||
|
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||||
|
},
|
||||||
|
createTimelineResult = { Result.success(pinnedEventsTimeline) },
|
||||||
|
)
|
||||||
|
val presenter = createPinnedMessagesListPresenter(room = room)
|
||||||
|
presenter.test {
|
||||||
|
val emptyState = consumeItemsUntilPredicate { state ->
|
||||||
|
state is PinnedMessagesListState.Empty
|
||||||
|
}.last()
|
||||||
|
assertThat(emptyState).isEqualTo(PinnedMessagesListState.Empty)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - unpin event`() = runTest {
|
fun `present - unpin event`() = runTest {
|
||||||
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
|
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
|
||||||
@@ -263,21 +354,18 @@ class PinnedMessagesListPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPinnedMessagesTimeline(): FakeTimeline {
|
private fun createPinnedMessagesTimeline(
|
||||||
val messageContent = aMessageContent("A message")
|
items: List<MatrixTimelineItem> = listOf(
|
||||||
return FakeTimeline(
|
|
||||||
timelineItems = flowOf(
|
|
||||||
listOf(
|
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = A_UNIQUE_ID,
|
uniqueId = A_UNIQUE_ID,
|
||||||
event = anEventTimelineItem(
|
event = anEventTimelineItem(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
content = messageContent,
|
content = aMessageContent("A message"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
)
|
): FakeTimeline {
|
||||||
)
|
return FakeTimeline(timelineItems = flowOf(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun roomPermissions(
|
private fun roomPermissions(
|
||||||
|
|||||||
+65
@@ -13,8 +13,12 @@ package io.element.android.features.messages.impl.timeline
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.onFirst
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollToIndex
|
import androidx.compose.ui.test.performScrollToIndex
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
@@ -29,8 +33,14 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
|
|||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
||||||
@@ -168,6 +178,61 @@ class TimelineViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
|
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on a reply to a loaded event emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
setTimelineView(
|
||||||
|
state = aTimelineState(
|
||||||
|
timelineItems = persistentListOf(
|
||||||
|
aTimelineItemEvent(
|
||||||
|
content = aTimelineItemTextContent(),
|
||||||
|
inReplyTo = InReplyToDetails.Ready(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
senderId = A_USER_ID,
|
||||||
|
senderProfile = aProfileDetailsReady(),
|
||||||
|
eventContent = MessageContent(
|
||||||
|
body = "Replied message",
|
||||||
|
inReplyTo = null,
|
||||||
|
isEdited = false,
|
||||||
|
threadInfo = null,
|
||||||
|
type = TextMessageType("Replied message", null),
|
||||||
|
),
|
||||||
|
textContent = "Replied message",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
eventsRecorder.clear()
|
||||||
|
|
||||||
|
onAllNodesWithText("Replied message").onFirst().performClick()
|
||||||
|
eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a reply to an event which could not be loaded renders a placeholder and does not emit any Event when clicked`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
setTimelineView(
|
||||||
|
state = aTimelineState(
|
||||||
|
timelineItems = persistentListOf(
|
||||||
|
aTimelineItemEvent(
|
||||||
|
content = aTimelineItemTextContent(),
|
||||||
|
inReplyTo = InReplyToDetails.Error(eventId = AN_EVENT_ID, message = "Failed to fetch the event"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onMessageClick = {},
|
||||||
|
)
|
||||||
|
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
|
||||||
|
eventsRecorder.clear()
|
||||||
|
|
||||||
|
onNodeWithText("Failed to fetch the event").assertDoesNotExist()
|
||||||
|
onNodeWithText(activity!!.getString(CommonStrings.error_message_not_found)).assertIsDisplayed().performClick()
|
||||||
|
eventsRecorder.assertEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
@Ignore(
|
@Ignore(
|
||||||
"performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." +
|
"performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." +
|
||||||
"This needs to be reworked to use a different approach to check the LoadMore event was emitted."
|
"This needs to be reworked to use a different approach to check the LoadMore event was emitted."
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalTestApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.rememberBottomSheetState
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ExpandSheetWhenImeIsVisibleTest : RobolectricTest() {
|
||||||
|
@Test
|
||||||
|
fun `the sheet is left alone while the keyboard is hidden`() = runAndroidComposeUiTest<ComponentActivity> {
|
||||||
|
val sheetState = renderWithIme(isImeVisible = false)
|
||||||
|
assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the sheet is expanded when the keyboard is shown`() = runAndroidComposeUiTest<ComponentActivity> {
|
||||||
|
val sheetState = renderWithIme(isImeVisible = true)
|
||||||
|
assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AndroidComposeUiTest<ComponentActivity>.renderWithIme(isImeVisible: Boolean): SheetState {
|
||||||
|
lateinit var sheetState: SheetState
|
||||||
|
lateinit var view: View
|
||||||
|
setContent {
|
||||||
|
view = LocalView.current
|
||||||
|
sheetState = rememberBottomSheetState(initialValue = SheetValue.PartiallyExpanded)
|
||||||
|
ExpandSheetWhenImeIsVisible(sheetState)
|
||||||
|
}
|
||||||
|
runOnIdle {
|
||||||
|
ViewCompat.dispatchApplyWindowInsets(
|
||||||
|
view,
|
||||||
|
WindowInsetsCompat.Builder()
|
||||||
|
.setVisible(WindowInsetsCompat.Type.ime(), isImeVisible)
|
||||||
|
.setInsets(WindowInsetsCompat.Type.ime(), if (isImeVisible) Insets.of(0, 0, 0, 300) else Insets.NONE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
waitForIdle()
|
||||||
|
return sheetState
|
||||||
|
}
|
||||||
+20
@@ -95,6 +95,24 @@ class TimelineProtectionPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - shown content is restored when the presenter is recreated`() = runTest {
|
||||||
|
val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Off, hideInviteAvatar = false)
|
||||||
|
val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig))
|
||||||
|
val timelineProtectionStore = DefaultTimelineProtectionStore()
|
||||||
|
createPresenter(mediaPreviewService = mediaPreviewService, timelineProtectionStore = timelineProtectionStore).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
|
||||||
|
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
createPresenter(mediaPreviewService = mediaPreviewService, timelineProtectionStore = timelineProtectionStore).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `present - validate media scans the media source`() = runTest {
|
fun `present - validate media scans the media source`() = runTest {
|
||||||
@@ -134,9 +152,11 @@ class TimelineProtectionPresenterTest {
|
|||||||
room: BaseRoom = FakeBaseRoom(),
|
room: BaseRoom = FakeBaseRoom(),
|
||||||
mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
||||||
contentScannerService: ContentScannerService = ContentScannerService { _: List<MediaSource>, _: ContentValidationState -> },
|
contentScannerService: ContentScannerService = ContentScannerService { _: List<MediaSource>, _: ContentValidationState -> },
|
||||||
|
timelineProtectionStore: TimelineProtectionStore = DefaultTimelineProtectionStore(),
|
||||||
) = TimelineProtectionPresenter(
|
) = TimelineProtectionPresenter(
|
||||||
mediaPreviewService = mediaPreviewService,
|
mediaPreviewService = mediaPreviewService,
|
||||||
room = room,
|
room = room,
|
||||||
contentScannerService = contentScannerService,
|
contentScannerService = contentScannerService,
|
||||||
|
timelineProtectionStore = timelineProtectionStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||||||
|
|
||||||
sealed interface BlockedUsersEvents {
|
sealed interface BlockedUsersEvents {
|
||||||
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
||||||
|
data class CopyToClipboard(val userId: UserId) : BlockedUsersEvents
|
||||||
data object ConfirmUnblock : BlockedUsersEvents
|
data object ConfirmUnblock : BlockedUsersEvents
|
||||||
data object Cancel : BlockedUsersEvents
|
data object Cancel : BlockedUsersEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -18,14 +18,19 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
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.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.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -34,6 +39,8 @@ import kotlinx.coroutines.launch
|
|||||||
class BlockedUsersPresenter(
|
class BlockedUsersPresenter(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<BlockedUsersState> {
|
) : Presenter<BlockedUsersState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): BlockedUsersState {
|
override fun present(): BlockedUsersState {
|
||||||
@@ -65,12 +72,18 @@ class BlockedUsersPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
||||||
fun handleEvent(event: BlockedUsersEvents) {
|
fun handleEvent(event: BlockedUsersEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is BlockedUsersEvents.Unblock -> {
|
is BlockedUsersEvents.Unblock -> {
|
||||||
pendingUserToUnblock = event.userId
|
pendingUserToUnblock = event.userId
|
||||||
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
|
is BlockedUsersEvents.CopyToClipboard -> {
|
||||||
|
clipboardHelper.copyPlainText(event.userId.value)
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||||
|
}
|
||||||
BlockedUsersEvents.ConfirmUnblock -> {
|
BlockedUsersEvents.ConfirmUnblock -> {
|
||||||
pendingUserToUnblock?.let {
|
pendingUserToUnblock?.let {
|
||||||
coroutineScope.unblockUser(it, unblockUserAction)
|
coroutineScope.unblockUser(it, unblockUserAction)
|
||||||
@@ -86,6 +99,7 @@ class BlockedUsersPresenter(
|
|||||||
return BlockedUsersState(
|
return BlockedUsersState(
|
||||||
blockedUsers = ignoredMatrixUser.toImmutableList(),
|
blockedUsers = ignoredMatrixUser.toImmutableList(),
|
||||||
unblockUserAction = unblockUserAction.value,
|
unblockUserAction = unblockUserAction.value,
|
||||||
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -9,11 +9,13 @@
|
|||||||
package io.element.android.features.preferences.impl.blockedusers
|
package io.element.android.features.preferences.impl.blockedusers
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class BlockedUsersState(
|
data class BlockedUsersState(
|
||||||
val blockedUsers: ImmutableList<MatrixUser>,
|
val blockedUsers: ImmutableList<MatrixUser>,
|
||||||
val unblockUserAction: AsyncAction<Unit>,
|
val unblockUserAction: AsyncAction<Unit>,
|
||||||
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
val eventSink: (BlockedUsersEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
@@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.blockedusers
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -30,11 +31,13 @@ class BlockedUsersStateProvider : PreviewParameterProvider<BlockedUsersState> {
|
|||||||
internal fun aBlockedUsersState(
|
internal fun aBlockedUsersState(
|
||||||
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
||||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
eventSink: (BlockedUsersEvents) -> Unit = {},
|
eventSink: (BlockedUsersEvents) -> Unit = {},
|
||||||
): BlockedUsersState {
|
): BlockedUsersState {
|
||||||
return BlockedUsersState(
|
return BlockedUsersState(
|
||||||
blockedUsers = blockedUsers.toImmutableList(),
|
blockedUsers = blockedUsers.toImmutableList(),
|
||||||
unblockUserAction = unblockUserAction,
|
unblockUserAction = unblockUserAction,
|
||||||
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.features.preferences.impl.blockedusers
|
package io.element.android.features.preferences.impl.blockedusers
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
@@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
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.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
@@ -43,6 +45,7 @@ fun BlockedUsersView(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -52,7 +55,8 @@ fun BlockedUsersView(
|
|||||||
BackButton(onClick = onBackClick)
|
BackButton(onClick = onBackClick)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding)
|
modifier = Modifier.padding(padding)
|
||||||
@@ -60,7 +64,8 @@ fun BlockedUsersView(
|
|||||||
items(state.blockedUsers) { matrixUser ->
|
items(state.blockedUsers) { matrixUser ->
|
||||||
BlockedUserItem(
|
BlockedUserItem(
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
|
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) },
|
||||||
|
onLongClick = { state.eventSink(BlockedUsersEvents.CopyToClipboard(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,9 +112,14 @@ fun BlockedUsersView(
|
|||||||
private fun BlockedUserItem(
|
private fun BlockedUserItem(
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
onClick: (UserId) -> Unit,
|
onClick: (UserId) -> Unit,
|
||||||
|
onLongClick: (UserId) -> Unit,
|
||||||
) {
|
) {
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = Modifier.clickable { onClick(matrixUser.userId) },
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = { onClick(matrixUser.userId) },
|
||||||
|
onLongClick = { onLongClick(matrixUser.userId) },
|
||||||
|
onLongClickLabel = stringResource(CommonStrings.action_copy),
|
||||||
|
),
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -74,8 +74,14 @@ class PreferencesRootPresenter(
|
|||||||
val showLinkNewDevice by remember {
|
val showLinkNewDevice by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
val isUserStatusEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus).collectAsState(initial = false)
|
|
||||||
val userStatusState = if (isUserStatusEnabled) userStatusPresenter.present() else null
|
val isUserStatusSupported by produceState(false) {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
||||||
|
.collect { isUserStatusEnabled ->
|
||||||
|
value = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val userStatusState = if (isUserStatusSupported) userStatusPresenter.present() else null
|
||||||
|
|
||||||
val otherSessions by remember {
|
val otherSessions by remember {
|
||||||
sessionStore.sessionsFlow().map { list ->
|
sessionStore.sessionsFlow().map { list ->
|
||||||
|
|||||||
+27
-2
@@ -8,10 +8,14 @@
|
|||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -25,7 +29,10 @@ import io.element.android.features.preferences.impl.R
|
|||||||
import io.element.android.features.preferences.impl.user.UserPreferences
|
import io.element.android.features.preferences.impl.user.UserPreferences
|
||||||
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||||
import io.element.android.features.preferences.impl.userstatus.UserStatusView
|
import io.element.android.features.preferences.impl.userstatus.UserStatusView
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionIndicator
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
@@ -71,9 +78,9 @@ fun PreferencesRootView(
|
|||||||
) {
|
) {
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
// Include pref from other modules
|
// Include pref from other modules
|
||||||
PreferencePage(
|
PreferencePage(
|
||||||
modifier = modifier,
|
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
title = stringResource(id = CommonStrings.common_settings),
|
title = stringResource(id = CommonStrings.common_settings),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
@@ -134,6 +141,22 @@ fun PreferencesRootView(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
state.userStatusState?.let {
|
||||||
|
UserStatusUpdateIndicator(it.updateStatusAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.UserStatusUpdateIndicator(updateStatusAction: AsyncAction<Unit>) {
|
||||||
|
AsyncActionIndicator(
|
||||||
|
asyncAction = updateStatusAction,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.statusBarsPadding(),
|
||||||
|
loading = { AsyncIndicator.Loading(text = stringResource(CommonStrings.common_saving)) },
|
||||||
|
failure = { _ -> AsyncIndicator.Failure(text = stringResource(CommonStrings.common_failed)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -315,7 +338,9 @@ private fun ColumnScope.GeneralSection(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Put developer settings at the end, so nothing bad happens if the user clicks 8 times to enable the entry
|
// Put developer settings at the end, so nothing bad happens if the user clicks 8 times to enable the entry
|
||||||
if (state.showDeveloperSettings) {
|
AnimatedVisibility(
|
||||||
|
visible = state.showDeveloperSettings,
|
||||||
|
) {
|
||||||
DeveloperPreferencesView(onOpenDeveloperSettings)
|
DeveloperPreferencesView(onOpenDeveloperSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -18,7 +18,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
|
||||||
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
|
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
@@ -38,6 +40,7 @@ class UserStatusPresenter(
|
|||||||
var isEmojiPickerVisible by remember { mutableStateOf(false) }
|
var isEmojiPickerVisible by remember { mutableStateOf(false) }
|
||||||
val customTextFieldState = rememberTextFieldState()
|
val customTextFieldState = rememberTextFieldState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val updateStatusAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
fun handleEvent(event: UserStatusEvent) {
|
fun handleEvent(event: UserStatusEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -47,11 +50,15 @@ class UserStatusPresenter(
|
|||||||
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
||||||
is UserStatusEvent.SetStatus -> {
|
is UserStatusEvent.SetStatus -> {
|
||||||
pickerState = UserStatusPickerState.Hidden
|
pickerState = UserStatusPickerState.Hidden
|
||||||
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
coroutineScope.launch {
|
||||||
|
updateStatusAction.runUpdatingState { matrixClient.setUserStatus(event.status) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UserStatusEvent.ClearStatus -> {
|
UserStatusEvent.ClearStatus -> {
|
||||||
pickerState = UserStatusPickerState.Hidden
|
pickerState = UserStatusPickerState.Hidden
|
||||||
coroutineScope.launch { matrixClient.clearUserStatus() }
|
coroutineScope.launch {
|
||||||
|
updateStatusAction.runUpdatingState { matrixClient.clearUserStatus() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UserStatusEvent.OpenCustomInput -> {
|
UserStatusEvent.OpenCustomInput -> {
|
||||||
val raw = userProfile.rawStatus
|
val raw = userProfile.rawStatus
|
||||||
@@ -93,6 +100,7 @@ class UserStatusPresenter(
|
|||||||
displayedStatus = userProfile.displayedStatus,
|
displayedStatus = userProfile.displayedStatus,
|
||||||
rawStatus = userProfile.rawStatus,
|
rawStatus = userProfile.rawStatus,
|
||||||
pickerState = effectivePickerState,
|
pickerState = effectivePickerState,
|
||||||
|
updateStatusAction = updateStatusAction.value,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
@@ -19,6 +20,7 @@ data class UserStatusState(
|
|||||||
val displayedStatus: DisplayedStatus?,
|
val displayedStatus: DisplayedStatus?,
|
||||||
val rawStatus: UserStatus?,
|
val rawStatus: UserStatus?,
|
||||||
val pickerState: UserStatusPickerState,
|
val pickerState: UserStatusPickerState,
|
||||||
|
val updateStatusAction: AsyncAction<Unit>,
|
||||||
val eventSink: (UserStatusEvent) -> Unit,
|
val eventSink: (UserStatusEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+3
@@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.userstatus
|
|||||||
|
|
||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
|
||||||
@@ -44,10 +45,12 @@ fun aUserStatusState(
|
|||||||
displayedStatus: DisplayedStatus? = null,
|
displayedStatus: DisplayedStatus? = null,
|
||||||
rawStatus: UserStatus? = null,
|
rawStatus: UserStatus? = null,
|
||||||
pickerState: UserStatusPickerState = UserStatusPickerState.Hidden,
|
pickerState: UserStatusPickerState = UserStatusPickerState.Hidden,
|
||||||
|
updateStatusAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (UserStatusEvent) -> Unit = {},
|
eventSink: (UserStatusEvent) -> Unit = {},
|
||||||
) = UserStatusState(
|
) = UserStatusState(
|
||||||
displayedStatus = displayedStatus,
|
displayedStatus = displayedStatus,
|
||||||
rawStatus = rawStatus,
|
rawStatus = rawStatus,
|
||||||
pickerState = pickerState,
|
pickerState = pickerState,
|
||||||
|
updateStatusAction = updateStatusAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Avatare in Chateinladungen ausblenden"</string>
|
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Avatare in Chateinladungen ausblenden"</string>
|
||||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Medienvorschau im Nachrichtenverlauf ausblenden"</string>
|
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Medienvorschau im Nachrichtenverlauf ausblenden"</string>
|
||||||
<string name="screen_advanced_settings_labs">"Labs"</string>
|
<string name="screen_advanced_settings_labs">"Labs"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_description">"Die Entfernung, die du zurücklegen musst, um ein Update auszulösen."</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_title">"Live-Standort-Updates"</string>
|
||||||
|
<plurals name="screen_advanced_settings_live_location_update_distance">
|
||||||
|
<item quantity="one">"Alle %1$d Meter"</item>
|
||||||
|
<item quantity="other">"Alle %1$d Meter"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_advanced_settings_media_compression_description">"Lade Fotos und Videos schneller hoch und reduziere den Datenverbrauch"</string>
|
<string name="screen_advanced_settings_media_compression_description">"Lade Fotos und Videos schneller hoch und reduziere den Datenverbrauch"</string>
|
||||||
<string name="screen_advanced_settings_media_compression_title">"Optimiere die Medienqualität"</string>
|
<string name="screen_advanced_settings_media_compression_title">"Optimiere die Medienqualität"</string>
|
||||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderation und Sicherheit"</string>
|
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderation und Sicherheit"</string>
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ Ha folytatja, egyes beállítások megváltozhatnak."</string>
|
|||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
|
||||||
<string name="screen_notification_settings_title">"Értesítések"</string>
|
<string name="screen_notification_settings_title">"Értesítések"</string>
|
||||||
|
<string name="screen_settings_user_status_away">"Távol"</string>
|
||||||
|
<string name="screen_settings_user_status_be_right_back">"Rögtön visszajövök"</string>
|
||||||
|
<string name="screen_settings_user_status_custom">"Egyedi…"</string>
|
||||||
|
<string name="screen_settings_user_status_custom_hint">"Egyéni állapot…"</string>
|
||||||
|
<string name="screen_settings_user_status_focus_time">"Feladatra koncentrál"</string>
|
||||||
|
<string name="screen_settings_user_status_in_a_meeting">"Értekezleten"</string>
|
||||||
|
<string name="screen_settings_user_status_on_the_road">"Úton"</string>
|
||||||
|
<string name="screen_settings_user_status_placeholder">"Mi az állapota?"</string>
|
||||||
<string name="theme_black">"Fekete"</string>
|
<string name="theme_black">"Fekete"</string>
|
||||||
<string name="theme_dark">"Sötét"</string>
|
<string name="theme_dark">"Sötét"</string>
|
||||||
<string name="theme_light">"Világos"</string>
|
<string name="theme_light">"Világos"</string>
|
||||||
|
|||||||
@@ -33,15 +33,15 @@
|
|||||||
<string name="screen_advanced_settings_push_provider_android">"Dostawca powiadomień push"</string>
|
<string name="screen_advanced_settings_push_provider_android">"Dostawca powiadomień push"</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie."</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts">"Potwierdzenia odczytania"</string>
|
<string name="screen_advanced_settings_send_read_receipts">"Potwierdzenia odczytania"</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts_description">"Gdy wyłączona, Twoje potwierdzenia odczytania nie zostaną wysłane. Potwierdzenia od innych wciąż będą odbierane."</string>
|
<string name="screen_advanced_settings_send_read_receipts_description">"Po wyłączeniu potwierdzenia odczytu nie będą wysyłane. Nadal będziesz otrzymywać potwierdzenia od innych użytkowników."</string>
|
||||||
<string name="screen_advanced_settings_share_presence">"Udostępnij obecność"</string>
|
<string name="screen_advanced_settings_share_presence">"Udostępnij obecność"</string>
|
||||||
<string name="screen_advanced_settings_share_presence_description">"Gdy wyłączona, nie będziesz mógł wysyłać lub odbierać potwierdzeń odczytu ani powiadomień pisania."</string>
|
<string name="screen_advanced_settings_share_presence_description">"Po wyłączeniu nie będzie można wysyłać ani odbierać potwierdzeń odczytu i powiadomień o pisaniu."</string>
|
||||||
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Zawsze ukrywaj"</string>
|
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Zawsze ukrywaj"</string>
|
||||||
<string name="screen_advanced_settings_show_media_timeline_always_show">"Zawsze pokazuj"</string>
|
<string name="screen_advanced_settings_show_media_timeline_always_show">"Zawsze pokazuj"</string>
|
||||||
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"W pokojach prywatnych"</string>
|
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"W pokojach prywatnych"</string>
|
||||||
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Ukryte media można zawsze wyświetlić, dotykając ich"</string>
|
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Ukryte media można zawsze wyświetlić, dotykając ich"</string>
|
||||||
<string name="screen_advanced_settings_show_media_timeline_title">"Pokaż media na osi czasu"</string>
|
<string name="screen_advanced_settings_show_media_timeline_title">"Pokaż media na osi czasu"</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Włącz opcję, aby wyświetlić źródło wiadomości na osi czasu."</string>
|
<string name="screen_advanced_settings_view_source_description">"Włącz, aby móc wyświetlać źródło wiadomości na osi czasu."</string>
|
||||||
<string name="screen_blocked_users_empty">"Nie blokujesz żadnych użytkowników"</string>
|
<string name="screen_blocked_users_empty">"Nie blokujesz żadnych użytkowników"</string>
|
||||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokuj"</string>
|
<string name="screen_blocked_users_unblock_alert_action">"Odblokuj"</string>
|
||||||
<string name="screen_blocked_users_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
|
<string name="screen_blocked_users_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
<string name="screen_labs_header_title">"Doriți experiențe noi?"</string>
|
<string name="screen_labs_header_title">"Doriți experiențe noi?"</string>
|
||||||
<string name="screen_labs_title">"Laboratoare"</string>
|
<string name="screen_labs_title">"Laboratoare"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Setări adiționale"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Setări adiționale"</string>
|
||||||
|
<string name="screen_notification_settings_call_ringtone_label">"Ton de apel"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Apeluri audio și video"</string>
|
<string name="screen_notification_settings_calls_label">"Apeluri audio și video"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"Nepotrivire de configurație"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"Nepotrivire de configurație"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch_description">"Am simplificat Setările pentru notificări pentru a face opțiunile mai ușor de găsit.
|
<string name="screen_notification_settings_configuration_mismatch_description">"Am simplificat Setările pentru notificări pentru a face opțiunile mai ușor de găsit.
|
||||||
@@ -79,14 +80,38 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate."</st
|
|||||||
<string name="screen_notification_settings_invite_for_me_label">"Invitații"</string>
|
<string name="screen_notification_settings_invite_for_me_label">"Invitații"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"Mențiuni"</string>
|
<string name="screen_notification_settings_mentions_section_title">"Mențiuni"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Alegeți un alt sunet…"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Se utilizează în prezent %1$s"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_title">"Sunet de mesaj"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_label">"Sunet de mesaj"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"Toate"</string>
|
<string name="screen_notification_settings_mode_all">"Toate"</string>
|
||||||
<string name="screen_notification_settings_mode_mentions">"Mențiuni"</string>
|
<string name="screen_notification_settings_mode_mentions">"Mențiuni"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"Anunță-mă pentru"</string>
|
<string name="screen_notification_settings_notification_section_title">"Anunță-mă pentru"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"Anunțați-mă pentru @room"</string>
|
<string name="screen_notification_settings_room_mention_label">"Anunțați-mă pentru @room"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_fallback">"Personalizat"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_sound_button_title">"Sunet personalizat…"</string>
|
||||||
|
<string name="screen_notification_settings_sound_delete_sound_error_title">"Eroare la ștergerea fișierului"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Element Implicit"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_fade">"Element Fade"</string>
|
||||||
|
<string name="screen_notification_settings_sound_import_sound_error_title">"Eroare la importul fișierului"</string>
|
||||||
|
<string name="screen_notification_settings_sound_preview_sound_error_title">"A fost intâmpinată o problemă la previzualizarea sunetului de alertă"</string>
|
||||||
|
<string name="screen_notification_settings_sound_section_title">"Sunet"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Ignorați eroarea privind sunetul de alertă"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_title">"A fost întâmpinată o problemă la setarea sunetului de alertă"</string>
|
||||||
|
<string name="screen_notification_settings_sound_silent">"Silențios"</string>
|
||||||
|
<string name="screen_notification_settings_sound_system_default">"Implicit sistem"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"Pentru a primi notificări, vă rugăm să vă schimbați %1$s."</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"Pentru a primi notificări, vă rugăm să vă schimbați %1$s."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Setări de sistem"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Setări de sistem"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Notificările de sistem sunt dezactivate"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Notificările de sistem sunt dezactivate"</string>
|
||||||
<string name="screen_notification_settings_title">"Notificări"</string>
|
<string name="screen_notification_settings_title">"Notificări"</string>
|
||||||
|
<string name="screen_settings_user_status_away">"Plecat"</string>
|
||||||
|
<string name="screen_settings_user_status_be_right_back">"Revin imediat"</string>
|
||||||
|
<string name="screen_settings_user_status_custom">"Personalizat…"</string>
|
||||||
|
<string name="screen_settings_user_status_custom_hint">"Status personalizat…"</string>
|
||||||
|
<string name="screen_settings_user_status_focus_time">"Timp de concentrare"</string>
|
||||||
|
<string name="screen_settings_user_status_in_a_meeting">"Într-o ședință"</string>
|
||||||
|
<string name="screen_settings_user_status_on_the_road">"Pe drum"</string>
|
||||||
|
<string name="screen_settings_user_status_placeholder">"Care este statusul dumneavoastră?"</string>
|
||||||
<string name="theme_black">"Negru"</string>
|
<string name="theme_black">"Negru"</string>
|
||||||
<string name="theme_dark">"Întunecat"</string>
|
<string name="theme_dark">"Întunecat"</string>
|
||||||
<string name="theme_light">"Deschis"</string>
|
<string name="theme_light">"Deschis"</string>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"在聊天室邀請請求中隱藏大頭貼"</string>
|
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"在聊天室邀請請求中隱藏大頭貼"</string>
|
||||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"在時間軸中隱藏媒體預覽"</string>
|
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"在時間軸中隱藏媒體預覽"</string>
|
||||||
<string name="screen_advanced_settings_labs">"實驗室"</string>
|
<string name="screen_advanced_settings_labs">"實驗室"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_description">"觸發更新所需的移動距離。"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer">"請確保已為此應用程式啟用「精確位置」功能。若要變更此權限,請前往 %1$s。"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer_link">"應用程式設定"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_title">"即時位置更新"</string>
|
||||||
|
<plurals name="screen_advanced_settings_live_location_update_distance">
|
||||||
|
<item quantity="other">"每 %1$d 公尺"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_advanced_settings_media_compression_description">"上傳照片與影片更快且減少資料使用量"</string>
|
<string name="screen_advanced_settings_media_compression_description">"上傳照片與影片更快且減少資料使用量"</string>
|
||||||
<string name="screen_advanced_settings_media_compression_title">"最佳化媒體品質"</string>
|
<string name="screen_advanced_settings_media_compression_title">"最佳化媒體品質"</string>
|
||||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"管理與安全"</string>
|
<string name="screen_advanced_settings_moderation_and_safety_section_title">"管理與安全"</string>
|
||||||
@@ -50,6 +57,7 @@
|
|||||||
<string name="screen_labs_header_title">"想不想來點新花樣?"</string>
|
<string name="screen_labs_header_title">"想不想來點新花樣?"</string>
|
||||||
<string name="screen_labs_title">"實驗室"</string>
|
<string name="screen_labs_title">"實驗室"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"其他設定"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"其他設定"</string>
|
||||||
|
<string name="screen_notification_settings_call_ringtone_label">"通話鈴聲"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"音訊與視訊通話"</string>
|
<string name="screen_notification_settings_calls_label">"音訊與視訊通話"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"組態錯誤"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"組態錯誤"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch_description">"我們簡化了通知設定,讓選項更容易搜尋。您過去選擇的某些自訂設定將不會在此顯示,但它們仍有作用。
|
<string name="screen_notification_settings_configuration_mismatch_description">"我們簡化了通知設定,讓選項更容易搜尋。您過去選擇的某些自訂設定將不會在此顯示,但它們仍有作用。
|
||||||
@@ -68,14 +76,39 @@
|
|||||||
<string name="screen_notification_settings_invite_for_me_label">"邀請"</string>
|
<string name="screen_notification_settings_invite_for_me_label">"邀請"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"您的家伺服器在加密聊天室中不支援此選項,可能無法收到部份聊天室的通知。"</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"您的家伺服器在加密聊天室中不支援此選項,可能無法收到部份聊天室的通知。"</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"提及"</string>
|
<string name="screen_notification_settings_mentions_section_title">"提及"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_choose_other">"選擇其他音效⋯⋯"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"目前正在使用 %1$s"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_title">"訊息音效"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_label">"訊息音效"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"全部"</string>
|
<string name="screen_notification_settings_mode_all">"全部"</string>
|
||||||
<string name="screen_notification_settings_mode_mentions">"提及"</string>
|
<string name="screen_notification_settings_mode_mentions">"提及"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"通知我"</string>
|
<string name="screen_notification_settings_notification_section_title">"通知我"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"於 @room 通知我"</string>
|
<string name="screen_notification_settings_room_mention_label">"於 @room 通知我"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_fallback">"自訂"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_sound_button_title">"自訂音效⋯⋯"</string>
|
||||||
|
<string name="screen_notification_settings_sound_delete_sound_error_title">"刪除檔案時發生錯誤"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Element 預設值"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_fade">"Element Fade"</string>
|
||||||
|
<string name="screen_notification_settings_sound_import_sound_error_title">"匯入檔案時發生錯誤"</string>
|
||||||
|
<string name="screen_notification_settings_sound_preview_sound_error_title">"預覽警示音時發生問題"</string>
|
||||||
|
<string name="screen_notification_settings_sound_section_title">"音效"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"忽略警示音錯誤"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_title">"設定警示音時發生問題"</string>
|
||||||
|
<string name="screen_notification_settings_sound_silent">"靜音"</string>
|
||||||
|
<string name="screen_notification_settings_sound_system_default">"系統預設值"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"要收到通知,請變更您的 %1$s。"</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"要收到通知,請變更您的 %1$s。"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"系統設定"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"系統設定"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"已關閉系統通知"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"已關閉系統通知"</string>
|
||||||
<string name="screen_notification_settings_title">"通知"</string>
|
<string name="screen_notification_settings_title">"通知"</string>
|
||||||
|
<string name="screen_settings_user_status_away">"離開"</string>
|
||||||
|
<string name="screen_settings_user_status_be_right_back">"稍後回來"</string>
|
||||||
|
<string name="screen_settings_user_status_custom">"自訂⋯⋯"</string>
|
||||||
|
<string name="screen_settings_user_status_custom_hint">"自訂狀態⋯⋯"</string>
|
||||||
|
<string name="screen_settings_user_status_focus_time">"專注時間"</string>
|
||||||
|
<string name="screen_settings_user_status_in_a_meeting">"在會議中"</string>
|
||||||
|
<string name="screen_settings_user_status_on_the_road">"在路上"</string>
|
||||||
|
<string name="screen_settings_user_status_placeholder">"您的狀態如何?"</string>
|
||||||
|
<string name="theme_black">"黑色"</string>
|
||||||
<string name="theme_dark">"深色"</string>
|
<string name="theme_dark">"深色"</string>
|
||||||
<string name="theme_light">"淺色"</string>
|
<string name="theme_light">"淺色"</string>
|
||||||
<string name="theme_system">"系統"</string>
|
<string name="theme_system">"系統"</string>
|
||||||
|
|||||||
+16
@@ -13,8 +13,10 @@ package io.element.android.features.preferences.impl.blockedusers
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.longClick
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTouchInput
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
@@ -57,6 +59,20 @@ class BlockedUserViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `long clicking on a user emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||||
|
val userList = aMatrixUserList()
|
||||||
|
setBlockedUsersView(
|
||||||
|
aBlockedUsersState(
|
||||||
|
blockedUsers = userList,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onNodeWithText(userList.first().displayName.orEmpty()).performTouchInput { longClick() }
|
||||||
|
eventsRecorder.assertSingle(BlockedUsersEvents.CopyToClipboard(userList.first().userId))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
|
fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||||
|
|||||||
+30
@@ -12,7 +12,9 @@ import app.cash.molecule.RecompositionMode
|
|||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
@@ -21,6 +23,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -169,6 +172,29 @@ class BlockedUsersPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - copy user id to clipboard`() = runTest {
|
||||||
|
val clipboardHelper = FakeClipboardHelper()
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
|
||||||
|
)
|
||||||
|
val presenter = aBlockedUsersPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
|
initialState.eventSink(BlockedUsersEvents.CopyToClipboard(A_USER_ID))
|
||||||
|
|
||||||
|
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_USER_ID.value)
|
||||||
|
assertThat(awaitItem().snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_copied_to_clipboard)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
||||||
val presenter = aBlockedUsersPresenter()
|
val presenter = aBlockedUsersPresenter()
|
||||||
@@ -183,8 +209,12 @@ class BlockedUsersPresenterTest {
|
|||||||
private fun aBlockedUsersPresenter(
|
private fun aBlockedUsersPresenter(
|
||||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
) = BlockedUsersPresenter(
|
) = BlockedUsersPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -8,9 +8,11 @@
|
|||||||
package io.element.android.features.preferences.impl.userstatus
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
|
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -61,6 +63,24 @@ class UserStatusPresenterTest {
|
|||||||
val newState = awaitItem()
|
val newState = awaitItem()
|
||||||
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||||
|
assertThat(newState.updateStatusAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(client.setUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Set event failure surfaces a Failure action`() = runTest {
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
setUserStatusResult = Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
createPresenter(client).test {
|
||||||
|
val status = UserStatus(emoji = "💬", text = "In a meeting")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||||
|
val newState = awaitItem()
|
||||||
|
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
// On failure the SDK does not emit a new profile, so nothing is displayed.
|
||||||
|
assertThat(newState.displayedStatus).isNull()
|
||||||
|
assertThat(newState.updateStatusAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat(client.setUserStatusCalled).isTrue()
|
assertThat(client.setUserStatusCalled).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +141,25 @@ class UserStatusPresenterTest {
|
|||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
assertThat(clearedState.displayedStatus).isNull()
|
assertThat(clearedState.displayedStatus).isNull()
|
||||||
|
assertThat(clearedState.updateStatusAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(client.clearUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Clear event failure surfaces a Failure action`() = runTest {
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
clearUserStatusResult = Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
createPresenter(client).test {
|
||||||
|
val status = UserStatus(emoji = "☕", text = "Be right back")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||||
|
awaitItem().eventSink(UserStatusEvent.ClearStatus)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
// Clear failed, so the previously set status is still displayed.
|
||||||
|
assertThat(clearedState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||||
|
assertThat(clearedState.updateStatusAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat(client.clearUserStatusCalled).isTrue()
|
assertThat(client.clearUserStatusCalled).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="screen_bug_report_editor_placeholder">"Descrieți problema…"</string>
|
<string name="screen_bug_report_editor_placeholder">"Descrieți problema…"</string>
|
||||||
<string name="screen_bug_report_editor_supporting">"Dacă posibil, vă rugăm să scrieți descrierea în engleză."</string>
|
<string name="screen_bug_report_editor_supporting">"Dacă posibil, vă rugăm să scrieți descrierea în engleză."</string>
|
||||||
<string name="screen_bug_report_error_description_too_short">"Descrierea este prea scurtă, vă rugăm să oferiți mai multe detalii despre ceea ce s-a întâmplat. Vă mulțumim!"</string>
|
<string name="screen_bug_report_error_description_too_short">"Descrierea este prea scurtă, vă rugăm să oferiți mai multe detalii despre ceea ce s-a întâmplat. Vă mulțumim!"</string>
|
||||||
|
<string name="screen_bug_report_github_issue_description">"Puteți introduce numărul unui tichet GitHub asociat, dacă există."</string>
|
||||||
|
<string name="screen_bug_report_github_issue_label">"Tichet GitHub"</string>
|
||||||
<string name="screen_bug_report_include_crash_logs">"Trimiteți log-uri"</string>
|
<string name="screen_bug_report_include_crash_logs">"Trimiteți log-uri"</string>
|
||||||
<string name="screen_bug_report_include_logs">"Permiteți log-uri"</string>
|
<string name="screen_bug_report_include_logs">"Permiteți log-uri"</string>
|
||||||
<string name="screen_bug_report_include_logs_error">"Jurnalele dumneavoastră sunt prea mari și nu pot fi incluse în acest raport. Vă rugăm să ni le trimiteți prin altă metodă."</string>
|
<string name="screen_bug_report_include_logs_error">"Jurnalele dumneavoastră sunt prea mari și nu pot fi incluse în acest raport. Vă rugăm să ni le trimiteți prin altă metodă."</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user