mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 10:32:34 +00:00
Merge
This commit is contained in:
@@ -7,7 +7,6 @@
|
|||||||
"PR-Dependencies",
|
"PR-Dependencies",
|
||||||
],
|
],
|
||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"string:app_name",
|
|
||||||
"gradle",
|
"gradle",
|
||||||
],
|
],
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ else
|
|||||||
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
||||||
fi
|
fi
|
||||||
git add -A
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Screenshots are already up to date, nothing to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
git commit -m "Update screenshots"
|
git commit -m "Update screenshots"
|
||||||
|
|
||||||
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ jobs:
|
|||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Build debug code and test fixtures
|
- name: Build debug code and test fixtures
|
||||||
|
|||||||
@@ -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' }}
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ __pycache__/
|
|||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
# Log Files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<component name="CopyrightManager">
|
<component name="CopyrightManager">
|
||||||
<copyright>
|
<copyright>
|
||||||
<option name="notice" value="© &#36;today.year Element Creations Ltd. Element Creations Ltd, Element Software SARL, Element Software Inc., and Element Software GmbH (the "Element Group") only make this file available under a proprietary license model. Without a proprietary license with us, you cannot use this file. The terms of the proprietary license agreement between you and any member of the Element Group shall always apply to your use of this file. Unauthorised use, copying, distribution, or modification of this file, via any medium, is strictly prohibited. For details about the licensing terms, you must either visit our website or contact a member of our sales team." />
|
<option name="notice" value="© &#36;today.year Element Creations Ltd. SPDX-License-Identifier: LicenseRef-Element-Commercial SPDX-License-Identifier: LicenseRef-Element-Proprietary" />
|
||||||
<option name="myName" value="Element Enterprise" />
|
<option name="myName" value="Element Enterprise" />
|
||||||
</copyright>
|
</copyright>
|
||||||
</component>
|
</component>
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
appId: ${MAESTRO_APP_ID}
|
|
||||||
---
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- takeScreenshot: build/maestro/200-ChangeServer
|
|
||||||
- tapOn: "matrix.org"
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- tapOn: "Other"
|
|
||||||
- tapOn:
|
|
||||||
id: "change_server-server"
|
|
||||||
- inputText: "element"
|
|
||||||
- hideKeyboard
|
|
||||||
- extendedWaitUntil:
|
|
||||||
visible: "element.io"
|
|
||||||
timeout: 10000
|
|
||||||
- tapOn: "element.io"
|
|
||||||
# Revert to matrix.org
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- tapOn: "matrix.org"
|
|
||||||
@@ -3,8 +3,7 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- tapOn: "Sign in manually"
|
- tapOn: "Sign in manually"
|
||||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
||||||
- takeScreenshot: build/maestro/100-SignIn
|
- takeScreenshot: build/maestro/100-SignIn
|
||||||
- runFlow: changeServer.yaml
|
# The account provider field defaults to matrix.org, so continue straight to the MAS login.
|
||||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "login-continue"
|
id: "login-continue"
|
||||||
## MAS page
|
## MAS page
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Change account provider"
|
visible: "Enter your account provider"
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ PRs must meet these rules.
|
|||||||
|
|
||||||
- Create previews for **all main states** of a Composable.
|
- Create previews for **all main states** of a Composable.
|
||||||
- Use `@PreviewsDayNight` for consistency.
|
- Use `@PreviewsDayNight` for consistency.
|
||||||
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
|
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states.
|
||||||
- Wrap previews in `ElementPreview { ... }`.
|
- Wrap previews in `ElementPreview { ... }`.
|
||||||
|
- When writing tests, never try to record the screenshots, the CI will do it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecul
|
|||||||
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
|
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
|
||||||
| `FooState.kt` | Data class representing the immutable UI state. |
|
| `FooState.kt` | Data class representing the immutable UI state. |
|
||||||
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
|
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
|
||||||
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
|
| `FooStatePreviewParam.kt` | Provides sample states for Previews and Screenshot tests. |
|
||||||
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
|
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+175
@@ -1,3 +1,178 @@
|
|||||||
|
Changes in Element X v26.08.2
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
|
||||||
|
|
||||||
|
**Note: this release in an internal release, it will not be pushed to the stores.**
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
|
||||||
|
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
|
||||||
|
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
|
||||||
|
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
|
||||||
|
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
|
||||||
|
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
|
||||||
|
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
|
||||||
|
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
|
||||||
|
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
|
||||||
|
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
|
||||||
|
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
|
||||||
|
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
|
||||||
|
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
|
||||||
|
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
|
||||||
|
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
|
||||||
|
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
|
||||||
|
### 🧱 Build
|
||||||
|
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
|
||||||
|
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
|
||||||
|
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
|
||||||
|
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
|
||||||
|
### 📄 Documentation
|
||||||
|
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
|
||||||
|
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
|
||||||
|
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
|
||||||
|
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
|
||||||
|
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
|
||||||
|
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
|
||||||
|
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
|
||||||
|
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
|
||||||
|
### 💼 Element Pro
|
||||||
|
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
|
||||||
|
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
|
||||||
|
### Others
|
||||||
|
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
|
||||||
|
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
|
||||||
|
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
|
||||||
|
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
|
||||||
|
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
|
||||||
|
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
|
||||||
|
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
|
||||||
|
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
|
||||||
|
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
|
||||||
|
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
|
||||||
|
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
|
||||||
|
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
|
||||||
|
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
|
||||||
|
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
|
||||||
|
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
|
||||||
|
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
|
||||||
|
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
|
||||||
|
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
|
||||||
|
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
|
||||||
|
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
|
||||||
|
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
|
||||||
|
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
|
||||||
|
|
||||||
|
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
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
|
|||||||
* Adds Node to the specified component graph.
|
* Adds Node to the specified component graph.
|
||||||
* Equivalent to the following declaration:
|
* Equivalent to the following declaration:
|
||||||
*
|
*
|
||||||
|
* ```
|
||||||
* @BindingContainer
|
* @BindingContainer
|
||||||
* @ContributesTo(Scope::class)
|
* @ContributesTo(Scope::class)
|
||||||
* abstract class YourNodeModule {
|
* interface YourNodeBindingContainer {
|
||||||
|
|
||||||
* @Binds
|
* @Binds
|
||||||
* @IntoMap
|
* @IntoMap
|
||||||
* @NodeKey(YourNode::class)
|
* @NodeKey(YourNode::class)
|
||||||
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
|
* fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
|
||||||
*}
|
* }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class ContributesNode(
|
annotation class ContributesNode(
|
||||||
|
|||||||
+28
-17
@@ -127,24 +127,11 @@ android {
|
|||||||
optimization {
|
optimization {
|
||||||
enable = true
|
enable = true
|
||||||
keepRules {
|
keepRules {
|
||||||
files.add(File(projectDir, "common-proguard-rules.pro"))
|
// Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
|
||||||
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
includeDefault = true
|
||||||
|
|
||||||
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
|
|
||||||
val flavorProguardFile = if (isEnterpriseBuild) {
|
|
||||||
// Custom rules for enterprise builds
|
|
||||||
File(projectDir, "enterprise-proguard-rules.pro")
|
|
||||||
} else {
|
|
||||||
// These default rules prevent the OSS app from being obfuscated
|
|
||||||
File(projectDir, "default-proguard-rules.pro")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flavorProguardFile.exists()) {
|
|
||||||
files.add(flavorProguardFile)
|
|
||||||
} else {
|
|
||||||
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Our custom keep rules are registered as `keepRules` source folders in the `androidComponents` block below,
|
||||||
|
// as the former `keepRules.files` DSL is deprecated since AGP 9.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +216,30 @@ androidComponents {
|
|||||||
)
|
)
|
||||||
|
|
||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
|
// Register the R8 keep rules source folders for optimized build types (release, nightly).
|
||||||
|
// Replaces the deprecated `optimization.keepRules.files` DSL (AGP 9+).
|
||||||
|
if (variant.buildType != "debug") {
|
||||||
|
variant.sources.keepRules?.let { keepRules ->
|
||||||
|
// Common rules, always applied.
|
||||||
|
keepRules.addStaticSourceDirectory("proguard/common")
|
||||||
|
|
||||||
|
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
|
||||||
|
val flavorProguardDir = if (isEnterpriseBuild) {
|
||||||
|
// Custom rules for enterprise builds
|
||||||
|
"../enterprise/proguard"
|
||||||
|
} else {
|
||||||
|
// Custom fules for FOSS builds
|
||||||
|
"proguard/foss"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File(projectDir, flavorProguardDir).exists()) {
|
||||||
|
keepRules.addStaticSourceDirectory(flavorProguardDir)
|
||||||
|
} else {
|
||||||
|
logger.warn("Proguard folder ${File(projectDir, flavorProguardDir).absolutePath} does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assigns a different version code for each output APK
|
// Assigns a different version code for each output APK
|
||||||
// other than the universal APK.
|
// other than the universal APK.
|
||||||
variant.outputs.forEach { output ->
|
variant.outputs.forEach { output ->
|
||||||
|
|||||||
+8
-7
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
|
|||||||
import dev.zacsweers.metro.Provides
|
import dev.zacsweers.metro.Provides
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.appconfig.ApplicationConfig
|
import io.element.android.appconfig.ApplicationConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||||
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -38,7 +38,7 @@ import java.io.File
|
|||||||
|
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
object AppModule {
|
object AppBindingContainer {
|
||||||
@Provides
|
@Provides
|
||||||
@BaseDirectory
|
@BaseDirectory
|
||||||
fun providesBaseDirectory(@ApplicationContext context: Context): File {
|
fun providesBaseDirectory(@ApplicationContext context: Context): File {
|
||||||
@@ -74,17 +74,18 @@ object AppModule {
|
|||||||
fun providesBuildMeta(
|
fun providesBuildMeta(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
buildType: BuildType,
|
buildType: BuildType,
|
||||||
enterpriseService: EnterpriseService,
|
isEnterpriseBuild: IsEnterpriseBuild,
|
||||||
): BuildMeta {
|
): BuildMeta {
|
||||||
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
|
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
|
||||||
|
val isEnterprise = isEnterpriseBuild()
|
||||||
return BuildMeta(
|
return BuildMeta(
|
||||||
isDebuggable = BuildConfig.DEBUG,
|
isDebuggable = BuildConfig.DEBUG,
|
||||||
buildType = buildType,
|
buildType = buildType,
|
||||||
applicationName = applicationName,
|
applicationName = applicationName,
|
||||||
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
||||||
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
||||||
applicationId = BuildConfig.APPLICATION_ID,
|
applicationId = BuildConfig.APPLICATION_ID,
|
||||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
isEnterpriseBuild = isEnterprise,
|
||||||
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
||||||
lowPrivacyLoggingEnabled = false,
|
lowPrivacyLoggingEnabled = false,
|
||||||
versionName = BuildConfig.VERSION_NAME,
|
versionName = BuildConfig.VERSION_NAME,
|
||||||
@@ -110,7 +111,7 @@ object AppModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
fun providesSnackbarDispatcher(): SnackbarDispatcher {
|
||||||
return SnackbarDispatcher()
|
return SnackbarDispatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-2025 New Vector 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.appconfig
|
|
||||||
|
|
||||||
object PushConfig {
|
|
||||||
/**
|
|
||||||
* Note: pusher_app_id cannot exceed 64 chars.
|
|
||||||
*/
|
|
||||||
const val PUSHER_APP_ID: String = "im.vector.app.android"
|
|
||||||
}
|
|
||||||
@@ -35,4 +35,11 @@ object RageshakeConfig {
|
|||||||
* The maximum number of log lines a rageshake can contain.
|
* The maximum number of log lines a rageshake can contain.
|
||||||
*/
|
*/
|
||||||
const val MAX_LOG_LINES_SIZE = 1_000_000
|
const val MAX_LOG_LINES_SIZE = 1_000_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
|
||||||
|
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
|
||||||
|
* mainly the list of the omitted log file names.
|
||||||
|
*/
|
||||||
|
const val MAX_DESCRIPTION_SIZE = 40 * 1024
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.libraries.uiCommon)
|
implementation(projects.libraries.uiCommon)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(projects.features.login.api)
|
implementation(projects.features.login.api)
|
||||||
|
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
@@ -60,16 +61,23 @@ 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.accountselect.test)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.matrixmedia.test)
|
||||||
testImplementation(projects.libraries.oauth.test)
|
testImplementation(projects.libraries.oauth.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.features.announcement.test)
|
||||||
testImplementation(projects.features.forward.test)
|
testImplementation(projects.features.forward.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.features.preferences.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
testImplementation(projects.features.signedout.test)
|
||||||
testImplementation(projects.services.apperror.test)
|
testImplementation(projects.services.apperror.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ package io.element.android.appnav
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -49,11 +51,13 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.home.api.HomeEntryPoint
|
import io.element.android.features.home.api.HomeEntryPoint
|
||||||
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
||||||
|
import io.element.android.features.location.api.LocalMapTilerConfig
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
@@ -153,6 +157,7 @@ class LoggedInFlowNode(
|
|||||||
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
|
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Placeholder,
|
initialElement = NavTarget.Placeholder,
|
||||||
@@ -330,11 +335,11 @@ class LoggedInFlowNode(
|
|||||||
}
|
}
|
||||||
NavTarget.Home -> {
|
NavTarget.Home -> {
|
||||||
val callback = object : HomeEntryPoint.Callback {
|
val callback = object : HomeEntryPoint.Callback {
|
||||||
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
|
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
attachRoom(
|
attachRoom(
|
||||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
|
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
|
||||||
clearBackstack = false,
|
clearBackstack = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -677,6 +682,12 @@ class LoggedInFlowNode(
|
|||||||
val colors by remember {
|
val colors by remember {
|
||||||
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
|
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
|
||||||
}.collectAsState(SemanticColorsLightDark.default)
|
}.collectAsState(SemanticColorsLightDark.default)
|
||||||
|
|
||||||
|
val currentMapTilerConfig = LocalMapTilerConfig.current
|
||||||
|
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
|
||||||
|
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
|
||||||
|
}
|
||||||
|
|
||||||
ElementThemeApp(
|
ElementThemeApp(
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
@@ -684,6 +695,7 @@ class LoggedInFlowNode(
|
|||||||
compoundDark = colors.dark,
|
compoundDark = colors.dark,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
) {
|
) {
|
||||||
|
CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) {
|
||||||
val isOnline by syncService.isOnline.collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
ConnectivityIndicatorContainer(
|
ConnectivityIndicatorContainer(
|
||||||
isOnline = isOnline,
|
isOnline = isOnline,
|
||||||
@@ -699,6 +711,7 @@ class LoggedInFlowNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
|
||||||
import io.element.android.appnav.intent.IntentResolver
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
import io.element.android.appnav.intent.ResolvedIntent
|
import io.element.android.appnav.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.room.RoomFlowNode
|
import io.element.android.appnav.room.RoomFlowNode
|
||||||
@@ -40,6 +39,7 @@ import io.element.android.appnav.room.RoomNavigationTarget
|
|||||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
import io.element.android.appnav.root.RootView
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
@@ -109,6 +109,13 @@ class RootFlowNode(
|
|||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
|
||||||
|
* Kept here so that the root nav target can be computed from both the logged in state and the pending
|
||||||
|
* login params, whatever the order in which the intent and the first nav state emission are processed.
|
||||||
|
*/
|
||||||
|
private var pendingLoginParams: LoginParams? = null
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
analyticsColdStartWatcher.start()
|
analyticsColdStartWatcher.start()
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
@@ -152,7 +159,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LoggedInState.NotLoggedIn -> {
|
LoggedInState.NotLoggedIn -> {
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(pendingLoginParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,15 +201,18 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
||||||
|
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
|
||||||
matrixSessionCache.removeAll()
|
matrixSessionCache.removeAll()
|
||||||
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +329,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +429,7 @@ class RootFlowNode(
|
|||||||
Timber.w("Login link ignored, multi account is disabled")
|
Timber.w("Login link ignored, multi account is disabled")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
pendingLoginParams = params
|
||||||
switchToNotLoggedInFlow(params)
|
switchToNotLoggedInFlow(params)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class IntentResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Intent.canBeIgnored(): Boolean {
|
private fun Intent.canBeIgnored(): Boolean {
|
||||||
|
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
|
||||||
return action == Intent.ACTION_MAIN &&
|
return action == Intent.ACTION_MAIN &&
|
||||||
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
|
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ class LoggedInPresenter(
|
|||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// Keep automatic call status (m.call) in sync with homeserver support.
|
||||||
|
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
|
matrixClient.enableAutomaticCallStatus(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
||||||
LaunchedEffect(networkConnectivity) {
|
LaunchedEffect(networkConnectivity) {
|
||||||
if (networkConnectivity == NetworkStatus.Connected) {
|
if (networkConnectivity == NetworkStatus.Connected) {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
|
|
||||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
|
||||||
override val values: Sequence<LoggedInState>
|
override val values: Sequence<LoggedInState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aLoggedInState(),
|
aLoggedInState(),
|
||||||
@@ -124,7 +124,7 @@ private fun ForceNativeSlidingSyncMigrationDialog(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStatePreviewParam::class) state: LoggedInState) = ElementPreview {
|
||||||
LoggedInView(
|
LoggedInView(
|
||||||
state = state,
|
state = state,
|
||||||
navigateToNotificationTroubleshoot = {},
|
navigateToNotificationTroubleshoot = {},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ 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.designsystem.utils.DelayedVisibility
|
import io.element.android.libraries.designsystem.utils.DelayedVisibility
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStatePreviewParam
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -84,7 +84,7 @@ private fun LoadingRoomTopBar(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStatePreviewParam::class) state: LoadingRoomState) = ElementPreview {
|
||||||
LoadingRoomNodeView(
|
LoadingRoomNodeView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {}
|
onBackClick = {}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
|
|||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
import com.bumble.appyx.core.state.SavedStateMap
|
import com.bumble.appyx.core.state.SavedStateMap
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import io.element.android.features.rageshake.api.detection.aRageshakeDetectionSt
|
|||||||
import io.element.android.services.apperror.api.AppErrorState
|
import io.element.android.services.apperror.api.AppErrorState
|
||||||
import io.element.android.services.apperror.api.aAppErrorState
|
import io.element.android.services.apperror.api.aAppErrorState
|
||||||
|
|
||||||
open class RootStateProvider : PreviewParameterProvider<RootState> {
|
open class RootStatePreviewParam : PreviewParameterProvider<RootState> {
|
||||||
override val values: Sequence<RootState>
|
override val values: Sequence<RootState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRootState().copy(
|
aRootState().copy(
|
||||||
@@ -60,7 +60,7 @@ fun RootView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
internal fun RootViewPreview(@PreviewParameter(RootStatePreviewParam::class) rootState: RootState) = ElementPreview {
|
||||||
RootView(
|
RootView(
|
||||||
state = rootState,
|
state = rootState,
|
||||||
onOpenBugReport = {},
|
onOpenBugReport = {},
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
* 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.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
@@ -115,7 +115,7 @@ class MatrixSessionCache(
|
|||||||
|
|
||||||
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
||||||
val syncOrchestrator = syncOrchestratorFactory.create(
|
val syncOrchestrator = syncOrchestratorFactory.create(
|
||||||
syncService = matrixClient.syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
|
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
|
||||||
)
|
)
|
||||||
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
||||||
+12
-5
@@ -6,7 +6,7 @@
|
|||||||
* 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.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
|||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.childScope
|
import io.element.android.libraries.core.coroutine.childScope
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.recordTransaction
|
import io.element.android.services.analytics.api.recordTransaction
|
||||||
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class SyncOrchestrator(
|
class SyncOrchestrator(
|
||||||
@Assisted private val syncService: SyncService,
|
@Assisted private val matrixClient: MatrixClient,
|
||||||
@Assisted sessionCoroutineScope: CoroutineScope,
|
@Assisted sessionCoroutineScope: CoroutineScope,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
@@ -47,13 +47,15 @@ class SyncOrchestrator(
|
|||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
syncService: SyncService,
|
matrixClient: MatrixClient,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
): SyncOrchestrator
|
): SyncOrchestrator
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "SyncOrchestrator"
|
private val tag = "SyncOrchestrator"
|
||||||
|
|
||||||
|
private val syncService = matrixClient.syncService
|
||||||
|
|
||||||
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
|
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
|
||||||
|
|
||||||
private val started = AtomicBoolean(false)
|
private val started = AtomicBoolean(false)
|
||||||
@@ -94,7 +96,7 @@ class SyncOrchestrator(
|
|||||||
appForegroundStateService.isInCall,
|
appForegroundStateService.isInCall,
|
||||||
appForegroundStateService.isSyncingNotificationEvent,
|
appForegroundStateService.isSyncingNotificationEvent,
|
||||||
appForegroundStateService.hasRingingCall,
|
appForegroundStateService.hasRingingCall,
|
||||||
appForegroundStateService.isSharingLiveLocation
|
appForegroundStateService.isSharingLiveLocation,
|
||||||
)
|
)
|
||||||
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
|
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
|
||||||
combine(
|
combine(
|
||||||
@@ -103,6 +105,11 @@ class SyncOrchestrator(
|
|||||||
networkMonitor.connectivity,
|
networkMonitor.connectivity,
|
||||||
isAppActiveFlow,
|
isAppActiveFlow,
|
||||||
) { syncState, networkState, isAppActive ->
|
) { syncState, networkState, isAppActive ->
|
||||||
|
if (matrixClient.isShuttingDown) {
|
||||||
|
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
|
||||||
|
return@combine SyncStateAction.NoOp
|
||||||
|
}
|
||||||
|
|
||||||
val isNetworkAvailable = networkState == NetworkStatus.Connected
|
val isNetworkAvailable = networkState == NetworkStatus.Connected
|
||||||
|
|
||||||
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
|
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_action">"تسجيل الخروج والترقية"</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"لم يعد %1$s يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"لم يعد خادمك المنزلي يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
|
||||||
|
</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="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string>
|
<string name="banner_migrate_to_native_sliding_sync_action">"خروج و ارتقا"</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s دیگر از شیوهنامهٔ قدیمی پشتیبانی نمیکند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."</string>
|
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s دیگر از شیوهنامهٔ قدیمی پشتیبانی نمیکند. لطفاً برای ادامهٔ استفاده از کاره، خارج شده و دوباره وارد شوید."</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"سرور خانگی شما دیگر از پروتکل قدیمی پشتیبانی نمیکند. لطفاً برای ادامه استفاده از برنامه، از سیستم خارج شده و دوباره وارد شوید."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import com.bumble.appyx.core.modality.AncestryInfo
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.node
|
||||||
|
import com.bumble.appyx.navmodel.backstack.activeElement
|
||||||
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
|
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
|
import io.element.android.appnav.root.RootPresenter
|
||||||
|
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
|
import io.element.android.features.login.api.LoginEntryPoint
|
||||||
|
import io.element.android.features.login.api.LoginParams
|
||||||
|
import io.element.android.features.login.test.FakeLoginEntryPoint
|
||||||
|
import io.element.android.features.login.test.FakeLoginIntentResolver
|
||||||
|
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
|
||||||
|
import io.element.android.features.preferences.test.FakeCacheService
|
||||||
|
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
|
||||||
|
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||||
|
import io.element.android.features.share.test.FakeShareIntentHandler
|
||||||
|
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
|
||||||
|
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||||
|
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
|
||||||
|
import io.element.android.services.apperror.test.FakeAppErrorStateService
|
||||||
|
import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings
|
||||||
|
import io.element.android.tests.testutils.node.FakeParentNode
|
||||||
|
import io.element.android.tests.testutils.presenter.NotUsedPresenter
|
||||||
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_LOGIN_LINK = "https://mobile.element.io/element/?account_provider=example.com&login_hint=mxid:@alice:example.com"
|
||||||
|
|
||||||
|
private val A_LOGIN_PARAMS = LoginParams(
|
||||||
|
accountProvider = "example.com",
|
||||||
|
loginHint = "mxid:@alice:example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_LOGIN_ENTRY_POINT_PARAMS = LoginEntryPoint.Params(
|
||||||
|
accountProvider = A_LOGIN_PARAMS.accountProvider,
|
||||||
|
loginHint = A_LOGIN_PARAMS.loginHint,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RootFlowNodeTest : RobolectricTest() {
|
||||||
|
@get:Rule
|
||||||
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The intent is handled first, this is what happens when the deep link cold starts the app.
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
// Then the first nav state emission lands, it must not override the login params.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled after the nav state is observed, then the login params are applied`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The nav state is observed first, this is what happens when the app is already running.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(null))
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aLoginIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(A_LOGIN_LINK))
|
||||||
|
|
||||||
|
private fun TestScope.createRootFlowNode(
|
||||||
|
loginEntryPoint: LoginEntryPoint,
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
): RootFlowNode {
|
||||||
|
val matrixSessionCache = MatrixSessionCache(
|
||||||
|
authenticationService = FakeMatrixAuthenticationService(),
|
||||||
|
syncOrchestratorFactory = FakeSyncOrchestratorFactory(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
)
|
||||||
|
val parentNode = FakeParentNode(
|
||||||
|
graph = FakeNodeFactoriesBindings(
|
||||||
|
mapOf(
|
||||||
|
NotLoggedInFlowNode::class to AssistedNodeFactory { buildContext, plugins ->
|
||||||
|
NotLoggedInFlowNode(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
loginEntryPoint = loginEntryPoint,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return RootFlowNode(
|
||||||
|
buildContext = BuildContext(
|
||||||
|
ancestryInfo = AncestryInfo.Child(anchor = parentNode),
|
||||||
|
savedStateMap = null,
|
||||||
|
customisations = NodeCustomisationDirectoryImpl(),
|
||||||
|
),
|
||||||
|
plugins = emptyList(),
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
accountProviderAccessControl = FakeAccountProviderAccessControl { true },
|
||||||
|
navStateFlowFactory = RootNavStateFlowFactory(
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
cacheService = FakeCacheService(),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
|
||||||
|
),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
presenter = RootPresenter(
|
||||||
|
crashDetectionPresenter = NotUsedPresenter(),
|
||||||
|
rageshakeDetectionPresenter = NotUsedPresenter(),
|
||||||
|
appErrorStateService = FakeAppErrorStateService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
sdkMetadata = FakeSdkMetadata("sha"),
|
||||||
|
),
|
||||||
|
bugReportEntryPoint = FakeBugReportEntryPoint(),
|
||||||
|
signedOutEntryPoint = FakeSignedOutEntryPoint(),
|
||||||
|
accountSelectEntryPoint = FakeAccountSelectEntryPoint(),
|
||||||
|
intentResolver = IntentResolver(
|
||||||
|
deeplinkParser = { null },
|
||||||
|
loginIntentResolver = FakeLoginIntentResolver { A_LOGIN_PARAMS },
|
||||||
|
oAuthIntentResolver = FakeOAuthIntentResolver { null },
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
shareIntentHandler = FakeShareIntentHandler(),
|
||||||
|
),
|
||||||
|
oAuthActionFlow = FakeOAuthActionFlow(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
announcementService = FakeAnnouncementService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
appCoroutineScope = backgroundScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import io.element.android.appnav.di.SyncOrchestrator
|
import io.element.android.appnav.session.SyncOrchestrator
|
||||||
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.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
|
|||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
|
|||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsSyncingNotificationEventValue = false,
|
initialIsSyncingNotificationEventValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsSyncingNotificationEventValue = false,
|
initialIsSyncingNotificationEventValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
|
|||||||
initialHasRingingCall = false,
|
initialHasRingingCall = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsInCallValue = true,
|
initialIsInCallValue = true,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsInCallValue = false,
|
initialIsInCallValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
|
|||||||
startSyncRecorder.assertions().isNeverCalled()
|
startSyncRecorder.assertions().isNeverCalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when the client is shutting down, the orchestrator does nothing`() = runTest {
|
||||||
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
|
val appForegroundStateService = FakeAppForegroundStateService(
|
||||||
|
initialForegroundValue = false,
|
||||||
|
initialIsSyncingNotificationEventValue = false,
|
||||||
|
initialIsInCallValue = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
|
||||||
|
startSyncLambda = startSyncRecorder
|
||||||
|
}
|
||||||
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
|
matrixClient = FakeMatrixClient(syncService = syncService, isShuttingDownResult = { true }),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
appForegroundStateService = appForegroundStateService,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We start observing
|
||||||
|
syncOrchestrator.observeStates()
|
||||||
|
|
||||||
|
// These should still not trigger a sync, sync the client is shutting down
|
||||||
|
networkMonitor.givenNetworkBlocked(false)
|
||||||
|
appForegroundStateService.updateIsInCallState(true)
|
||||||
|
appForegroundStateService.isInForeground.value = true
|
||||||
|
|
||||||
|
advanceTimeBy(10.seconds)
|
||||||
|
|
||||||
|
startSyncRecorder.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createSyncOrchestrator(
|
private fun TestScope.createSyncOrchestrator(
|
||||||
syncService: FakeSyncService = FakeSyncService(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
) = SyncOrchestrator(
|
) = SyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
|
|||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve intent replayed from the history should return null`() {
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
deeplinkParserResult = DeeplinkData.Room(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = null,
|
||||||
|
eventId = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve navigation intent root`() {
|
fun `test resolve navigation intent root`() {
|
||||||
val sut = createIntentResolver(
|
val sut = createIntentResolver(
|
||||||
|
|||||||
@@ -48,6 +48,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
|
||||||
@@ -346,6 +347,44 @@ 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,
|
||||||
|
).test {
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
assert(enableAutomaticCallStatusLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enables automatic call status when server supports it`() = runTest {
|
||||||
|
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
|
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
|
||||||
|
).apply {
|
||||||
|
isUserStatusSupportedResult = Result.success(true)
|
||||||
|
}
|
||||||
|
createLoggedInPresenter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
).test {
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
assert(enableAutomaticCallStatusLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(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()
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.appnav.session
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class FakeSyncOrchestratorFactory : SyncOrchestrator.Factory {
|
||||||
|
override fun create(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SyncOrchestrator = lambdaError()
|
||||||
|
}
|
||||||
+4
-4
@@ -6,12 +6,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.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
|
|||||||
|
|
||||||
return object : SyncOrchestrator.Factory {
|
return object : SyncOrchestrator.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
syncService: SyncService,
|
matrixClient: MatrixClient,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
): SyncOrchestrator {
|
): SyncOrchestrator {
|
||||||
return SyncOrchestrator(
|
return SyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
appForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService = FakeAppForegroundStateService(),
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
+1
-1
@@ -98,7 +98,7 @@ allprojects {
|
|||||||
// This is disabled by default, but the CI will enforce this.
|
// This is disabled by default, but the CI will enforce this.
|
||||||
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
||||||
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
||||||
allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
|
allWarningsAsErrors = findProperty("allWarningsAsErrors") == "true"
|
||||||
|
|
||||||
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
||||||
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.sonarqube.gradle.SonarResolverTask
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
* Copyright 2022-2025 New Vector Ltd.
|
* Copyright 2022-2025 New Vector Ltd.
|
||||||
@@ -18,3 +20,9 @@ dependencies {
|
|||||||
implementation(libs.ksp.plugin)
|
implementation(libs.ksp.plugin)
|
||||||
implementation(libs.kotlinpoet.ksp)
|
implementation(libs.kotlinpoet.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the processResources task is executed before the SonarResolverTask to avoid issues with missing resources during SonarQube analysis.
|
||||||
|
// This *should not be necesssary* but is a workaround for a known issue with the SonarQube Gradle plugin.
|
||||||
|
tasks.withType<SonarResolverTask>().configureEach {
|
||||||
|
dependsOn("processResources")
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,32 +61,32 @@ class ContributesNodeProcessor(
|
|||||||
if (config.enableLogging) {
|
if (config.enableLogging) {
|
||||||
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
|
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
|
||||||
}
|
}
|
||||||
generateModule(ksClass)
|
generateBindingContainer(ksClass)
|
||||||
generateFactory(ksClass)
|
generateFactory(ksClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
return invalidSymbols
|
return invalidSymbols
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateModule(ksClass: KSClassDeclaration) {
|
private fun generateBindingContainer(ksClass: KSClassDeclaration) {
|
||||||
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
|
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
|
||||||
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
||||||
val modulePackage = ksClass.packageName.asString()
|
val bindingContainerPackage = ksClass.packageName.asString()
|
||||||
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
val bindingContainerClassName = "${ksClass.simpleName.asString()}BindingContainer"
|
||||||
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||||
val content = FileSpec.builder(
|
val content = FileSpec.builder(
|
||||||
packageName = modulePackage,
|
packageName = bindingContainerPackage,
|
||||||
fileName = moduleClassName,
|
fileName = bindingContainerClassName,
|
||||||
)
|
)
|
||||||
.addType(
|
.addType(
|
||||||
TypeSpec.interfaceBuilder(moduleClassName)
|
TypeSpec.interfaceBuilder(bindingContainerClassName)
|
||||||
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
||||||
.addAnnotation(BindingContainer::class)
|
.addAnnotation(BindingContainer::class)
|
||||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
||||||
.addFunction(
|
.addFunction(
|
||||||
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
FunSpec.builder("bind${ksClass.simpleName.asString()}AssistedFactory")
|
||||||
.addModifiers(KModifier.ABSTRACT)
|
.addModifiers(KModifier.ABSTRACT)
|
||||||
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory"))
|
.addParameter("factory", ClassName(bindingContainerPackage, "${ksClass.simpleName.asString()}AssistedFactory"))
|
||||||
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
|
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
|
||||||
.addAnnotation(Binds::class)
|
.addAnnotation(Binds::class)
|
||||||
.addAnnotation(IntoMap::class)
|
.addAnnotation(IntoMap::class)
|
||||||
@@ -114,7 +114,7 @@ class ContributesNodeProcessor(
|
|||||||
@OptIn(KspExperimental::class)
|
@OptIn(KspExperimental::class)
|
||||||
private fun generateFactory(ksClass: KSClassDeclaration) {
|
private fun generateFactory(ksClass: KSClassDeclaration) {
|
||||||
val generatedPackage = ksClass.packageName.asString()
|
val generatedPackage = ksClass.packageName.asString()
|
||||||
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
|
val assistedFactoryClassName = "${ksClass.simpleName.asString()}AssistedFactory"
|
||||||
val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
|
val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
|
||||||
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
||||||
if (assistedParameters.size != 2) {
|
if (assistedParameters.size != 2) {
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|
||||||
@@ -320,9 +322,11 @@ Example for a new feature called RoomDetails:
|
|||||||
4. Verify that the structure looks ok and click on Finish
|
4. Verify that the structure looks ok and click on Finish
|
||||||
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, StatePreviewParam, 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>StatePreviewParam.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
|
||||||
@@ -356,9 +360,9 @@ We have 3 tests frameworks in place, and this should be sufficient to guarantee
|
|||||||
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
|
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
|
||||||
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
|
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
|
||||||
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
|
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
|
||||||
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewProvider to provide
|
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewParam classes to provide
|
||||||
different states. See for instance the
|
different states. See for instance the
|
||||||
file [TemplateStateProvider.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt)
|
file [TemplateStatePreviewParam.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStatePreviewParam.kt)
|
||||||
- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
|
- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
|
||||||
|
|
||||||
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
|
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ Rust sdk branch: https://github.com/matrix-org/matrix-rust-sdk/tree/oidc-ffi
|
|||||||
|
|
||||||
Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
|
Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
|
||||||
|
|
||||||
Server list: https://github.com/element-hq/oidc-playground
|
|
||||||
|
|
||||||
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
|
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
|
||||||
|
|
||||||
clientName: InfoPlistReader.main.bundleDisplayName,
|
clientName: InfoPlistReader.main.bundleDisplayName,
|
||||||
|
|||||||
+1
-1
Submodule enterprise updated: 029db99513...e633a343b1
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.api.preferences
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<AnalyticsPreferencesState> {
|
open class AnalyticsPreferencesStatePreviewParam : PreviewParameterProvider<AnalyticsPreferencesState> {
|
||||||
override val values: Sequence<AnalyticsPreferencesState>
|
override val values: Sequence<AnalyticsPreferencesState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAnalyticsPreferencesState().copy(isEnabled = true),
|
aAnalyticsPreferencesState().copy(isEnabled = true),
|
||||||
+1
-1
@@ -66,7 +66,7 @@ fun AnalyticsPreferencesView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStatePreviewParam::class) state: AnalyticsPreferencesState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
AnalyticsPreferencesView(
|
AnalyticsPreferencesView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"هنا"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"مشاركة البيانات الإحصائية"</string>
|
||||||
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.impl
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class AnalyticsOptInStateProvider : PreviewParameterProvider<AnalyticsOptInState> {
|
open class AnalyticsOptInStatePreviewParam : PreviewParameterProvider<AnalyticsOptInState> {
|
||||||
override val values: Sequence<AnalyticsOptInState>
|
override val values: Sequence<AnalyticsOptInState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAnalyticsOptInState(),
|
aAnalyticsOptInState(),
|
||||||
+1
-1
@@ -171,7 +171,7 @@ private fun AnalyticsOptInFooter(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview {
|
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStatePreviewParam::class) state: AnalyticsOptInState) = ElementPreview {
|
||||||
AnalyticsOptInView(
|
AnalyticsOptInView(
|
||||||
state = state,
|
state = state,
|
||||||
onClickTerms = {},
|
onClickTerms = {},
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
interface AnalyticsModule {
|
interface AnalyticsBindingContainer {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
|
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"لن نسجّل أو ننشئ ملفات شخصية لأي معلومات شخصية"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"هنا"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"يمكنك إطفاء هذا الخيار في أي وقت"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"لن نشارك بياناتك مع أطراف ثالثة"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"ساعدنا على التحسن %1$s"</string>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
||||||
|
|||||||
+19
@@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues the full-screen announcements shown to the user once, such as the notice about a new feature.
|
||||||
|
*
|
||||||
|
* A dismissed announcement is remembered so it is not shown again.
|
||||||
|
*/
|
||||||
interface AnnouncementService {
|
interface AnnouncementService {
|
||||||
|
/**
|
||||||
|
* Queues an announcement, unless the user has already dismissed it.
|
||||||
|
*
|
||||||
|
* @param announcement the announcement to show.
|
||||||
|
*/
|
||||||
suspend fun showAnnouncement(announcement: Announcement)
|
suspend fun showAnnouncement(announcement: Announcement)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that the user dismissed an announcement, so it is not queued again.
|
||||||
|
*
|
||||||
|
* @param announcement the announcement that was dismissed.
|
||||||
|
*/
|
||||||
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
suspend fun onAnnouncementDismissed(announcement: Announcement)
|
||||||
|
|
||||||
|
/** The announcements still waiting to be shown, in the order they should appear. */
|
||||||
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
fun announcementsToShowFlow(): Flow<List<Announcement>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this composable to render the announcement UI in Fullscreen.
|
* Use this composable to render the announcement UI in Fullscreen.
|
||||||
|
* Draws nothing while there is no announcement to show.
|
||||||
|
*
|
||||||
|
* @param modifier layout modifier for the container.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.announcement.impl
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
|
||||||
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> {
|
open class AnnouncementStatePreviewParam : PreviewParameterProvider<AnnouncementState> {
|
||||||
override val values: Sequence<AnnouncementState>
|
override val values: Sequence<AnnouncementState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAnnouncementState(),
|
anAnnouncementState(),
|
||||||
+2
-2
@@ -33,7 +33,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
import io.element.android.features.announcement.impl.AnnouncementEvent
|
import io.element.android.features.announcement.impl.AnnouncementEvent
|
||||||
import io.element.android.features.announcement.impl.AnnouncementState
|
import io.element.android.features.announcement.impl.AnnouncementState
|
||||||
import io.element.android.features.announcement.impl.AnnouncementStateProvider
|
import io.element.android.features.announcement.impl.AnnouncementStatePreviewParam
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
@@ -216,7 +216,7 @@ private fun Announcement.Fullscreen.notice(): String? = when (this) {
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
|
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStatePreviewParam::class) state: AnnouncementState) = ElementPreview {
|
||||||
FullscreenAnnouncementView(
|
FullscreenAnnouncementView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
package io.element.android.features.cachecleaner.api
|
package io.element.android.features.cachecleaner.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the temporary files holding decrypted content, so plaintext media does not linger on disk.
|
||||||
|
*/
|
||||||
interface CacheCleaner {
|
interface CacheCleaner {
|
||||||
/**
|
/**
|
||||||
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
|
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
|
||||||
|
|||||||
+3
@@ -10,6 +10,9 @@ package io.element.android.features.call.api
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it.
|
||||||
|
*/
|
||||||
interface CurrentCallService {
|
interface CurrentCallService {
|
||||||
/**
|
/**
|
||||||
* The current call state flow, which will be updated when the active call changes.
|
* The current call state flow, which will be updated when the active call changes.
|
||||||
|
|||||||
+2
@@ -11,7 +11,9 @@ package io.element.android.features.call.impl.pip
|
|||||||
import io.element.android.features.call.impl.utils.PipController
|
import io.element.android.features.call.impl.utils.PipController
|
||||||
|
|
||||||
sealed interface PictureInPictureEvent {
|
sealed interface PictureInPictureEvent {
|
||||||
|
data object OnCallStarted : PictureInPictureEvent
|
||||||
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
||||||
data object EnterPictureInPicture : PictureInPictureEvent
|
data object EnterPictureInPicture : PictureInPictureEvent
|
||||||
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
||||||
|
data class SetPipOrientation(val orientation: Int?) : PictureInPictureEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -38,6 +38,9 @@ class PictureInPicturePresenter(
|
|||||||
|
|
||||||
fun handleEvent(event: PictureInPictureEvent) {
|
fun handleEvent(event: PictureInPictureEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is PictureInPictureEvent.OnCallStarted -> {
|
||||||
|
pipController?.setupPipCallbacks()
|
||||||
|
}
|
||||||
is PictureInPictureEvent.SetPipController -> {
|
is PictureInPictureEvent.SetPipController -> {
|
||||||
pipController = event.pipController
|
pipController = event.pipController
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,12 @@ class PictureInPicturePresenter(
|
|||||||
pipController?.exitPip()
|
pipController?.exitPip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is PictureInPictureEvent.SetPipOrientation -> {
|
||||||
|
Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}")
|
||||||
|
if (isPipSupported) {
|
||||||
|
pipView?.setPipOrientation(event.orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -8,8 +8,15 @@
|
|||||||
|
|
||||||
package io.element.android.features.call.impl.pip
|
package io.element.android.features.call.impl.pip
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
interface PipView {
|
interface PipView {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun setPipOrientation(orientation: Int?)
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun setPipParams()
|
fun setPipParams()
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun enterPipMode(): Boolean
|
fun enterPipMode(): Boolean
|
||||||
fun hangUp()
|
fun hangUp()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> {
|
open class CallNotificationDataPreviewParam : PreviewParameterProvider<CallNotificationData> {
|
||||||
override val values: Sequence<CallNotificationData>
|
override val values: Sequence<CallNotificationData>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCallNotificationData(
|
aCallNotificationData(
|
||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.call.impl.ui
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
open class CallScreenStatePreviewParam : PreviewParameterProvider<CallScreenState> {
|
||||||
override val values: Sequence<CallScreenState>
|
override val values: Sequence<CallScreenState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCallScreenState(),
|
aCallScreenState(),
|
||||||
+8
-2
@@ -118,6 +118,7 @@ internal fun CallScreenView(
|
|||||||
if (webViewAudioManager?.isInCallMode?.get() == false) {
|
if (webViewAudioManager?.isInCallMode?.get() == false) {
|
||||||
Timber.d("URL $url is loaded, starting in-call audio mode")
|
Timber.d("URL $url is loaded, starting in-call audio mode")
|
||||||
webViewAudioManager?.onCallStarted()
|
webViewAudioManager?.onCallStarted()
|
||||||
|
pipState.eventSink(PictureInPictureEvent.OnCallStarted)
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Can't start in-call audio mode since the app is already in it.")
|
Timber.d("Can't start in-call audio mode since the app is already in it.")
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,12 @@ internal fun CallScreenView(
|
|||||||
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
||||||
)
|
)
|
||||||
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
|
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
|
||||||
val pipController = WebViewPipController(webView)
|
val pipController = WebViewPipController(
|
||||||
|
webView = webView,
|
||||||
|
updatePipOrientation = { orientation ->
|
||||||
|
pipState.eventSink(PictureInPictureEvent.SetPipOrientation(orientation))
|
||||||
|
}
|
||||||
|
)
|
||||||
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
||||||
},
|
},
|
||||||
onDestroyWebView = {
|
onDestroyWebView = {
|
||||||
@@ -260,7 +266,7 @@ private fun WebView.dispatchEscKeyEvent() {
|
|||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenViewPreview(
|
internal fun CallScreenViewPreview(
|
||||||
@PreviewParameter(CallScreenStateProvider::class) state: CallScreenState,
|
@PreviewParameter(CallScreenStatePreviewParam::class) state: CallScreenState,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
+16
-3
@@ -11,9 +11,9 @@ package io.element.android.features.call.impl.ui
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Rational
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -49,6 +49,7 @@ import io.element.android.features.call.impl.pip.PipView
|
|||||||
import io.element.android.features.call.impl.services.CallForegroundService
|
import io.element.android.features.call.impl.services.CallForegroundService
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
|
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
|
||||||
|
import io.element.android.libraries.androidutils.media.setAspectRatioFromOrientation
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.audio.api.AudioFocus
|
import io.element.android.libraries.audio.api.AudioFocus
|
||||||
@@ -86,6 +87,8 @@ class ElementCallActivity :
|
|||||||
|
|
||||||
private var eventSink: ((CallScreenEvent) -> Unit)? = null
|
private var eventSink: ((CallScreenEvent) -> Unit)? = null
|
||||||
|
|
||||||
|
private var currentPipOrientation: Int? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -199,6 +202,9 @@ class ElementCallActivity :
|
|||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
|
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
setPipParams()
|
||||||
|
}
|
||||||
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
|
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
|
||||||
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
|
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
|
||||||
@@ -296,11 +302,18 @@ class ElementCallActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
override fun setPipOrientation(orientation: Int?) {
|
||||||
|
currentPipOrientation = orientation
|
||||||
|
setPictureInPictureParams(getPictureInPictureParams())
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun getPictureInPictureParams(): PictureInPictureParams {
|
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||||
|
// Portrait for calls seems more appropriate as a fallback value
|
||||||
|
val orientation = currentPipOrientation ?: Configuration.ORIENTATION_PORTRAIT
|
||||||
return PictureInPictureParams.Builder()
|
return PictureInPictureParams.Builder()
|
||||||
// Portrait for calls seems more appropriate
|
.setAspectRatioFromOrientation(orientation)
|
||||||
.setAspectRatio(Rational(3, 5))
|
|
||||||
.apply {
|
.apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
setAutoEnterEnabled(true)
|
setAutoEnterEnabled(true)
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ private fun ActionButton(
|
|||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IncomingCallScreenPreview(
|
internal fun IncomingCallScreenPreview(
|
||||||
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
|
@PreviewParameter(CallNotificationDataPreviewParam::class) state: CallNotificationData,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
IncomingCallScreen(
|
IncomingCallScreen(
|
||||||
notificationData = state,
|
notificationData = state,
|
||||||
|
|||||||
+1
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.call.impl.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
interface PipController {
|
interface PipController {
|
||||||
|
fun setupPipCallbacks()
|
||||||
suspend fun canEnterPip(): Boolean
|
suspend fun canEnterPip(): Boolean
|
||||||
fun enterPip()
|
fun enterPip()
|
||||||
fun exitPip()
|
fun exitPip()
|
||||||
|
|||||||
+1
-1
@@ -250,7 +250,7 @@ class WebViewAudioManager(
|
|||||||
|
|
||||||
hasRegisteredCallbacks = true
|
hasRegisteredCallbacks = true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Timber.d("Setting androidNativeBridge javascript interface in webview")
|
Timber.d("Setting androidNativeBridge javascript interface in webview")
|
||||||
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
|
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
|
||||||
|
|||||||
+30
@@ -8,7 +8,10 @@
|
|||||||
|
|
||||||
package io.element.android.features.call.impl.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@@ -18,7 +21,18 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
*/
|
*/
|
||||||
class WebViewPipController(
|
class WebViewPipController(
|
||||||
private val webView: WebView,
|
private val webView: WebView,
|
||||||
|
updatePipOrientation: (orientation: Int?) -> Unit,
|
||||||
) : PipController {
|
) : PipController {
|
||||||
|
init {
|
||||||
|
// Register the JavaScript interface ahead of time to ensure it's available when the WebView content is loaded and tries to call it
|
||||||
|
webView.addJavascriptInterface(PipBridge(updatePipOrientation), "androidPipBridge")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPipCallbacks() {
|
||||||
|
Timber.d("Adding callback in controls.onPipMediaOrientationUpdate")
|
||||||
|
webView.evaluateJavascript("controls.onPipMediaOrientationUpdate = (orientation) => { androidPipBridge.setPipOrientation(orientation); };", null)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun canEnterPip(): Boolean {
|
override suspend fun canEnterPip(): Boolean {
|
||||||
return suspendCoroutine { continuation ->
|
return suspendCoroutine { continuation ->
|
||||||
webView.evaluateJavascript("controls.canEnterPip()") { result ->
|
webView.evaluateJavascript("controls.canEnterPip()") { result ->
|
||||||
@@ -36,3 +50,19 @@ class WebViewPipController(
|
|||||||
webView.evaluateJavascript("controls.disablePip()", null)
|
webView.evaluateJavascript("controls.disablePip()", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PipBridge(
|
||||||
|
private val onOrientationChange: (orientation: Int?) -> Unit,
|
||||||
|
) {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun setPipOrientation(orientation: String?) {
|
||||||
|
Timber.d("Picture-in-picture orientation changed in webview, orientation: $orientation")
|
||||||
|
// This callback is used to update the picture-in-picture orientation in the WebView when it changes
|
||||||
|
val orientation = when (orientation) {
|
||||||
|
"portrait" -> Configuration.ORIENTATION_PORTRAIT
|
||||||
|
"landscape" -> Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
onOrientationChange(orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"مكالمة جارية"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"اضغط للعودة إلى المكالمة"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ الاتصال يتمّ"</string>
|
||||||
|
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"لا يدعم Element Call استخدام أجهزة الصوت عبر Bluetooth في إصدار Android هذا. يرجى اختيار جهاز صوت آخر."</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"مكالمة Element Call واردة"</string>
|
||||||
|
</resources>
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
|
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
|
||||||
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
|
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
|
||||||
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
|
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
|
||||||
|
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"برنامه Element Call در این نسخه اندروید از دستگاههای صوتی بلوتوث پشتیبانی نمیکند. لطفاً دستگاه صوتی دیگری را انتخاب کنید."</string>
|
||||||
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
|
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+3
@@ -12,10 +12,13 @@ import io.element.android.features.call.impl.utils.PipController
|
|||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakePipController(
|
class FakePipController(
|
||||||
|
private val setupPipCallbacksResult: () -> Unit = { lambdaError() },
|
||||||
private val canEnterPipResult: () -> Boolean = { lambdaError() },
|
private val canEnterPipResult: () -> Boolean = { lambdaError() },
|
||||||
private val enterPipResult: () -> Unit = { lambdaError() },
|
private val enterPipResult: () -> Unit = { lambdaError() },
|
||||||
private val exitPipResult: () -> Unit = { lambdaError() },
|
private val exitPipResult: () -> Unit = { lambdaError() },
|
||||||
) : PipController {
|
) : PipController {
|
||||||
|
override fun setupPipCallbacks() = setupPipCallbacksResult()
|
||||||
|
|
||||||
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
|
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
|
||||||
|
|
||||||
override fun enterPip() = enterPipResult()
|
override fun enterPip() = enterPipResult()
|
||||||
|
|||||||
+3
-1
@@ -13,9 +13,11 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||||||
class FakePipView(
|
class FakePipView(
|
||||||
private val setPipParamsResult: () -> Unit = { lambdaError() },
|
private val setPipParamsResult: () -> Unit = { lambdaError() },
|
||||||
private val enterPipModeResult: () -> Boolean = { lambdaError() },
|
private val enterPipModeResult: () -> Boolean = { lambdaError() },
|
||||||
private val handUpResult: () -> Unit = { lambdaError() }
|
private val handUpResult: () -> Unit = { lambdaError() },
|
||||||
|
private val onOrientationChangeResult: (orientation: Int?) -> Unit = { lambdaError() },
|
||||||
) : PipView {
|
) : PipView {
|
||||||
override fun setPipParams() = setPipParamsResult()
|
override fun setPipParams() = setPipParamsResult()
|
||||||
override fun enterPipMode(): Boolean = enterPipModeResult()
|
override fun enterPipMode(): Boolean = enterPipModeResult()
|
||||||
override fun hangUp() = handUpResult()
|
override fun hangUp() = handUpResult()
|
||||||
|
override fun setPipOrientation(orientation: Int?) = onOrientationChangeResult(orientation)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
|
|||||||
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
||||||
*/
|
*/
|
||||||
fun interface ContentScannerService {
|
fun interface ContentScannerService {
|
||||||
|
/**
|
||||||
|
* Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning.
|
||||||
|
*
|
||||||
|
* @param mediaSources the media to scan.
|
||||||
|
* @param contentValidationState where the outcome of each scan is published for the UI to observe.
|
||||||
|
*/
|
||||||
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
|
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
object ContentScannerModule {
|
object ContentScannerBindingContainer {
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(RoomScope::class)
|
@SingleIn(RoomScope::class)
|
||||||
fun providesContentScannerService(
|
fun providesContentScannerService(
|
||||||
+5
-5
@@ -15,16 +15,16 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ContentScannerModuleTest {
|
class ContentScannerBindingContainerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
fun `ContentScannerBindingContainer provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val contentScanner: ContentScanner? = null
|
val contentScanner: ContentScanner? = null
|
||||||
val sessionCoroutineScope = backgroundScope
|
val sessionCoroutineScope = backgroundScope
|
||||||
val coroutineDispatchers = testCoroutineDispatchers()
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
|
||||||
contentScanner = contentScanner,
|
contentScanner = contentScanner,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
@@ -35,14 +35,14 @@ class ContentScannerModuleTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
fun `ContentScannerBindingContainer provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val contentScanner: ContentScanner? = FakeContentScanner()
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
val sessionCoroutineScope = backgroundScope
|
val sessionCoroutineScope = backgroundScope
|
||||||
val coroutineDispatchers = testCoroutineDispatchers()
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
|
||||||
contentScanner = contentScanner,
|
contentScanner = contentScanner,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
+12
@@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
/**
|
||||||
|
* Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens.
|
||||||
|
*/
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
/**
|
||||||
|
* @param isSpace true to create a space rather than a room.
|
||||||
|
*/
|
||||||
fun setIsSpace(isSpace: Boolean): Builder
|
fun setIsSpace(isSpace: Boolean): Builder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param parentSpaceId the space the new room should be added to once created.
|
||||||
|
*/
|
||||||
fun setParentSpace(parentSpaceId: RoomId): Builder
|
fun setParentSpace(parentSpaceId: RoomId): Builder
|
||||||
|
|
||||||
|
/** Builds the node with the options configured so far. */
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
@@ -86,7 +86,7 @@ private fun AddPeopleTopBar(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStatePreviewParam::class) state: InvitePeopleState) = ElementPreview {
|
||||||
AddPeopleView(
|
AddPeopleView(
|
||||||
state = state,
|
state = state,
|
||||||
invitePeopleView = {},
|
invitePeopleView = {},
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState
|
|||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStatePreviewParam : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
override val values: Sequence<ConfigureRoomState>
|
override val values: Sequence<ConfigureRoomState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aConfigureRoomState(),
|
aConfigureRoomState(),
|
||||||
+2
-2
@@ -349,12 +349,12 @@ private fun RoomJoinRuleOptions(
|
|||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
@ExcludeFromCoverage
|
@ExcludeFromCoverage
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_room_action_create_room">"إنشاء غرفة جديدة"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"دعوة أشخاص"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"حصل خطأ أثناء إنشاء الغرفة"</string>
|
||||||
|
<string name="screen_create_room_error_creating_space">"تعذّر إنشاء المساحة بسبب خطأ غير معروف. حاول مرة أخرى لاحقًا."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"إضافة اسم…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"غرفة جديدة"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"مساحة جديدة"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"لا يدخل إلا المدعوّون."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"خاصة"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"بإمكان الجميع العثور على هذه الغرفة.
|
||||||
|
يمكنك تغيير هذا من إعدادات الغرفة متى شئت."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"يمكن لأي شخص الانضمام."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"عامة"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"يمكن لأي شخص طلب الانضمام، لكن يجب أن يقبل الطلب مسؤول أو مشرف."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"السماح بطلبات الانضمام"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام، لكن يجب على الآخرين طلب الوصول."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"طلب الانضمام"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"يمكن للمدعوين فقط الانضمام."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"خاصة"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_description">"يمكن لأي شخص الانضمام."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_title">"عامة"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_title">"قياسي"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"من يمكنه الوصول"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"تحتاج إلى عنوان حتى تظهر الغرفة في الدليل العام."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"العنوان"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"ظهور الغرفة"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(لا توجد مساحة)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_option">"عدم الإضافة إلى مساحة"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"لم يتم اختيار أي مساحة"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"إضافة إلى مساحة"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"الموضوع (اختياري)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"إضافة وصف…"</string>
|
||||||
|
</resources>
|
||||||
@@ -3,14 +3,34 @@
|
|||||||
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
|
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Invitar personas"</string>
|
<string name="screen_create_room_add_people_title">"Invitar personas"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string>
|
<string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string>
|
||||||
|
<string name="screen_create_room_error_creating_space">"No se pudo crear el espacio debido a un error desconocido. Inténtalo de nuevo más tarde."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Añadir nombre…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Nueva sala"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Nuevo espacio"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string>
|
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Privado"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
|
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
|
||||||
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
|
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Cualquiera puede unirse."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Público"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Cualquiera en %1$s puede unirse, pero el resto debe solicitar acceso."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Solicitar unirse"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"Solo las personas invitadas pueden unirse."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"Privado"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_title">"Público"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Cualquiera de %1$s puede unirse."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Estándar"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Quién tiene acceso"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string>
|
<string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Dirección"</string>
|
<string name="screen_create_room_room_address_section_title">"Dirección"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(ningún espacio)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_option">"No añadir a un espacio"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"No se ha seleccionado ningún espacio"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Añadir a un espacio"</string>
|
||||||
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
|
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Añadir descripción…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,12 +3,34 @@
|
|||||||
<string name="screen_create_room_action_create_room">"اتاق جدید"</string>
|
<string name="screen_create_room_action_create_room">"اتاق جدید"</string>
|
||||||
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
|
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
|
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
|
||||||
|
<string name="screen_create_room_error_creating_space">"به دلیل خطای ناشناختهای، فضا ایجاد نشد. بعداً دوباره امتحان کنید."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"افزودن نام…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"اتاق جدید"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"فضای جدید"</string>
|
||||||
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده میتوانند بپیوندند."</string>
|
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده میتوانند بپیوندند."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"خصوصی"</string>
|
||||||
<string name="screen_create_room_public_option_description">"هرکسی میتواند اتاق را بیابد.
|
<string name="screen_create_room_public_option_description">"هرکسی میتواند اتاق را بیابد.
|
||||||
میتوانید بعداً در تظیمات اتاق عوضش کنید."</string>
|
میتوانید بعداً در تظیمات اتاق عوضش کنید."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string>
|
<string name="screen_create_room_public_option_short_description">"هر کسی میتواند عضو شود."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"هرکسی میتواند به این اتاق بپیوندد"</string>
|
<string name="screen_create_room_public_option_title">"عمومی"</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"هر کسی میتواند درخواست عضویت بدهد، اما یک مدیر یا ناظر باید درخواست را بپذیرد."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"اجازه درخواست عضویت"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"هر کسی در %1$s میتواند عضو شود، اما بقیه باید درخواست دسترسی بدهند."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"درخواست عضویت"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"تنها افراد دعوت شده میتوانند بپیوندند."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"خصوصی"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_description">"هر کسی میتواند عضو شود."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_title">"عمومی"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_description">"هر کسی که در %1$s باشد میتواند عضو شود."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_title">"استاندارد"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"چه کسی دسترسی دارد"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"برای اینکه بتوانید آن را در فهرست عمومی مشاهده کنید، به یک آدرس نیاز دارید."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"آدرس"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
|
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(بدون فضا)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_option">"به یک فضا اضافه نکنید"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"فضایی انتخاب نشده است"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"به فضا اضافه کنید"</string>
|
||||||
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
|
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"اضافه کردن توضیحات…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.logout.impl
|
|||||||
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
|
||||||
|
|
||||||
open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDeactivationState> {
|
open class AccountDeactivationStatePreviewParam : PreviewParameterProvider<AccountDeactivationState> {
|
||||||
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
|
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
|
||||||
override val values: Sequence<AccountDeactivationState>
|
override val values: Sequence<AccountDeactivationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
+3
-3
@@ -299,16 +299,16 @@ private fun Content(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the string does not contain any new line characters, which can happen when pasting values.
|
* Ensure that the string does not contain any line separator, which can happen when pasting values.
|
||||||
*/
|
*/
|
||||||
private fun String.sanitize(): String {
|
private fun String.sanitize(): String {
|
||||||
return replace("\n", "")
|
return filterNot { it == '\n' || it == '\r' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AccountDeactivationViewPreview(
|
internal fun AccountDeactivationViewPreview(
|
||||||
@PreviewParameter(AccountDeactivationStateProvider::class) state: AccountDeactivationState,
|
@PreviewParameter(AccountDeactivationStatePreviewParam::class) state: AccountDeactivationState,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
AccountDeactivationView(
|
AccountDeactivationView(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_deactivate_account_confirmation_dialog_content">"يرجى تأكيد رغبتك في حذف حسابك. لا يمكن التراجع عن هذا الإجراء."</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages">"حذف جميع رسائلي"</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages_notice">"تحذير: قد يرى المستخدمون في المستقبل محادثات غير مكتملة."</string>
|
||||||
|
<string name="screen_deactivate_account_description">"حذف حسابك %1$s، وسيؤدي إلى:"</string>
|
||||||
|
<string name="screen_deactivate_account_description_bold_part">"إجراء لا رجعة فيه"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1">"%1$s (لن تتمكن من تسجيل الدخول مرة أخرى، ولا يمكن إعادة استخدام معرّفك)."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1_bold_part">"تعطيل حسابك نهائيًا"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_2">"إزالتك من جميع غرف الدردشة."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_3">"حذف معلومات حسابك من خادم الهوية لدينا."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_4">"ستظل رسائلك مرئية للمستخدمين المسجلين، لكنها لن تكون متاحة للمستخدمين الجدد أو غير المسجلين إذا اخترت حذفها."</string>
|
||||||
|
<string name="screen_deactivate_account_title">"حذف الحساب"</string>
|
||||||
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user