Merge branch 'develop' into renovate/datastore
This commit is contained in:
commit
5a64b87b5b
1523 changed files with 20510 additions and 7576 deletions
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@12.1.0
|
uses: danger/danger-js@12.3.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
4
.github/workflows/generate_github_pages.yml
vendored
4
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -22,10 +22,10 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Run World screenshots generation script
|
- name: Run World screenshots generation script
|
||||||
run: |
|
run: |
|
||||||
./tools/test/generateWorldScreenshots.py
|
./tools/test/generateWorldScreenshots.py
|
||||||
|
|
|
||||||
40
.github/workflows/maestro.yml
vendored
40
.github/workflows/maestro.yml
vendored
|
|
@ -12,12 +12,10 @@ env:
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
maestro-cloud:
|
build-apk:
|
||||||
name: Maestro test suite
|
name: Build APK
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
# Allow one per PR.
|
# Allow one per PR.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('maestro-{0}', github.ref) }}
|
group: ${{ format('maestro-{0}', github.ref) }}
|
||||||
|
|
@ -41,19 +39,49 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
run: ./gradlew :app:assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:assembleGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
- name: Upload APK as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: elementx-apk-maestro
|
||||||
|
path: |
|
||||||
|
app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
|
||||||
|
retention-days: 5
|
||||||
|
overwrite: true
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
maestro-cloud:
|
||||||
|
name: Maestro test suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-apk
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run-Maestro'
|
||||||
|
# Allow one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ format('maestro-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Download APK artifact from previous job
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: elementx-apk-maestro
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
||||||
app-file: app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
|
app-file: app-gplay-x86_64-debug.apk
|
||||||
env: |
|
env: |
|
||||||
MAESTRO_USERNAME=maestroelement
|
MAESTRO_USERNAME=maestroelement
|
||||||
MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
- name: 📈 Generate kover report and verify coverage
|
- name: 📈 Generate kover report and verify coverage
|
||||||
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
- name: ✅ Upload kover report
|
- name: ✅ Upload kover report
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
||||||
153
.github/workflows/quality.yml
vendored
153
.github/workflows/quality.yml
vendored
|
|
@ -10,7 +10,7 @@ on:
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
env:
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon --warn
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checkScript:
|
checkScript:
|
||||||
|
|
@ -26,19 +26,20 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Search for invalid screenshot files
|
- name: Search for invalid screenshot files
|
||||||
run: ./tools/test/checkInvalidScreenshots.py
|
run: ./tools/test/checkInvalidScreenshots.py
|
||||||
|
|
||||||
check:
|
# Code checks
|
||||||
name: Project Check Suite
|
konsist:
|
||||||
|
name: Konsist tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Allow all jobs on main and develop. Just one per PR.
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -55,8 +56,40 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run Konsist tests
|
||||||
run: ./gradlew runQualityChecks $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||||
|
- name: Upload reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: konsist-report
|
||||||
|
path: |
|
||||||
|
**/build/reports/**/*.*
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Android lint check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
with:
|
||||||
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
- name: Run lint
|
||||||
|
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -64,6 +97,108 @@ jobs:
|
||||||
name: linting-report
|
name: linting-report
|
||||||
path: |
|
path: |
|
||||||
**/build/reports/**/*.*
|
**/build/reports/**/*.*
|
||||||
|
|
||||||
|
detekt:
|
||||||
|
name: Detekt checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
with:
|
||||||
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
- name: Run Detekt
|
||||||
|
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||||
|
- name: Upload reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: detekt-report
|
||||||
|
path: |
|
||||||
|
**/build/reports/**/*.*
|
||||||
|
|
||||||
|
ktlint:
|
||||||
|
name: Ktlint checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
with:
|
||||||
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
- name: Run Ktlint check
|
||||||
|
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Upload reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ktlint-report
|
||||||
|
path: |
|
||||||
|
**/build/reports/**/*.*
|
||||||
|
|
||||||
|
knit:
|
||||||
|
name: Knit checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
with:
|
||||||
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
- name: Run Knit
|
||||||
|
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
|
upload_reports:
|
||||||
|
name: Project Check Suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [konsist, lint, ktlint, detekt]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
|
- name: Download reports from previous jobs
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
- name: Prepare Danger
|
- name: Prepare Danger
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -72,7 +207,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@12.1.0
|
uses: danger/danger-js@12.3.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: Create release App Bundle
|
name: Create release App Bundle and APKs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
@ -11,11 +11,11 @@ env:
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
gplay:
|
||||||
name: Create App Bundle
|
name: Create App Bundle (Gplay)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }}
|
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-gplay-{0}', github.sha) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -38,3 +38,31 @@ jobs:
|
||||||
name: elementx-app-gplay-bundle-unsigned
|
name: elementx-app-gplay-bundle-unsigned
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
|
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
|
||||||
|
|
||||||
|
fdroid:
|
||||||
|
name: Create APKs (FDroid)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-fdroid-{0}', github.sha) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
- name: Create APKs
|
||||||
|
env:
|
||||||
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Upload apks as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: elementx-app-fdroid-apks-unsigned
|
||||||
|
path: |
|
||||||
|
app/build/outputs/apk/fdroid/release/*.apk
|
||||||
|
|
|
||||||
5
.github/workflows/sync-localazy.yml
vendored
5
.github/workflows/sync-localazy.yml
vendored
|
|
@ -21,10 +21,10 @@ jobs:
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Setup Localazy
|
- name: Setup Localazy
|
||||||
run: |
|
run: |
|
||||||
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
|
curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg
|
||||||
|
|
@ -33,6 +33,7 @@ jobs:
|
||||||
- name: Run Localazy script
|
- name: Run Localazy script
|
||||||
run: |
|
run: |
|
||||||
./tools/localazy/downloadStrings.sh --all
|
./tools/localazy/downloadStrings.sh --all
|
||||||
|
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||||
./tools/test/generateAllScreenshots.py
|
./tools/test/generateAllScreenshots.py
|
||||||
- name: Create Pull Request for Strings
|
- name: Create Pull Request for Strings
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
|
|
||||||
4
.github/workflows/sync-sas-strings.yml
vendored
4
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -13,10 +13,10 @@ jobs:
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.12
|
||||||
- name: Install Prerequisite dependencies
|
- name: Install Prerequisite dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install requests
|
pip install requests
|
||||||
|
|
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -55,7 +55,7 @@ jobs:
|
||||||
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
- name: 📈Generate kover report and verify coverage
|
- name: 📈Generate kover report and verify coverage
|
||||||
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
||||||
- name: 🚫 Upload kover failed coverage reports
|
- name: 🚫 Upload kover failed coverage reports
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -45,6 +45,7 @@ captures/
|
||||||
.idea/assetWizardSettings.xml
|
.idea/assetWizardSettings.xml
|
||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
.idea/deploymentTargetDropDown.xml
|
.idea/deploymentTargetDropDown.xml
|
||||||
|
.idea/deploymentTargetSelector.xml
|
||||||
.idea/gradle.xml
|
.idea/gradle.xml
|
||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
.idea/misc.xml
|
.idea/misc.xml
|
||||||
|
|
|
||||||
2
.idea/dictionaries/shared.xml
generated
2
.idea/dictionaries/shared.xml
generated
|
|
@ -3,7 +3,9 @@
|
||||||
<words>
|
<words>
|
||||||
<w>backstack</w>
|
<w>backstack</w>
|
||||||
<w>blurhash</w>
|
<w>blurhash</w>
|
||||||
|
<w>fdroid</w>
|
||||||
<w>ftue</w>
|
<w>ftue</w>
|
||||||
|
<w>gplay</w>
|
||||||
<w>homeserver</w>
|
<w>homeserver</w>
|
||||||
<w>konsist</w>
|
<w>konsist</w>
|
||||||
<w>kover</w>
|
<w>kover</w>
|
||||||
|
|
|
||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.23" />
|
<option name="version" value="1.9.24" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -16,7 +16,7 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- tapOn: "Create"
|
- tapOn: "Create"
|
||||||
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
||||||
- tapOn: "aRoomName"
|
- tapOn: "aRoomName"
|
||||||
- tapOn: "Invite people"
|
- tapOn: "Invite"
|
||||||
# assert there's 1 member and 1 invitee
|
# assert there's 1 member and 1 invitee
|
||||||
- tapOn: "Search for someone"
|
- tapOn: "Search for someone"
|
||||||
- inputText: ${MAESTRO_INVITEE2_MXID}
|
- inputText: ${MAESTRO_INVITEE2_MXID}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- takeScreenshot: build/maestro/510-Timeline
|
- takeScreenshot: build/maestro/510-Timeline
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "rich_text_editor"
|
id: "text_editor"
|
||||||
- inputText: "Hello world!"
|
- inputText: "Hello world!"
|
||||||
- tapOn: "Send"
|
- tapOn: "Send"
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ appId: ${MAESTRO_APP_ID}
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
text: "Advanced settings"
|
text: "Advanced settings"
|
||||||
- assertVisible: "Rich text editor"
|
- assertVisible: "View source"
|
||||||
- back
|
- back
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
|
||||||
65
CHANGES.md
65
CHANGES.md
|
|
@ -1,3 +1,64 @@
|
||||||
|
Changes in Element X v0.4.13 (2024-05-22)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Add plain text editor based on Markdown input. ([#2840](https://github.com/element-hq/element-x-android/issues/2840))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Use members display names for their membership state events. ([#2286](https://github.com/element-hq/element-x-android/issues/2286))
|
||||||
|
- Make sure explicit links in messages take priority over links found by linkification (urls, emails, phone numbers, etc.) ([#2291](https://github.com/element-hq/element-x-android/issues/2291))
|
||||||
|
- Fix modal contents overlapping screen lock pin. ([#2692](https://github.com/element-hq/element-x-android/issues/2692))
|
||||||
|
- Fix a crash when trying to create an `EncryptedFile` in Android 6. ([#2846](https://github.com/element-hq/element-x-android/issues/2846))
|
||||||
|
- Session falsely displayed as 'verified' with no internet connection. ([#2884](https://github.com/element-hq/element-x-android/issues/2884))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Allow configuring push notification provider ([#2340](https://github.com/element-hq/element-x-android/issues/2340))
|
||||||
|
- UX cleanup: reorder text composer actions to prioritise camera ones. ([#2803](https://github.com/element-hq/element-x-android/issues/2803))
|
||||||
|
- Translation added into Portuguese and Simplified Chinese ([#2834](https://github.com/element-hq/element-x-android/issues/2834))
|
||||||
|
- Use via parameters when joining a room from permalink. ([#2843](https://github.com/element-hq/element-x-android/issues/2843))
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Element X v0.4.12 (2024-05-13)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Add support for expected decryption errors due to membership (UX and analytics). ([#2754](https://github.com/element-hq/element-x-android/issues/2754))
|
||||||
|
- Handle permalink navigation to Events. ([#2759](https://github.com/element-hq/element-x-android/issues/2759))
|
||||||
|
- Pretty-print event JSON in debug viewer ([#2771](https://github.com/element-hq/element-x-android/issues/2771))
|
||||||
|
- Add support for external permalinks. ([#2776](https://github.com/element-hq/element-x-android/issues/2776))
|
||||||
|
- Enable support for Android per-app language preferences ([#2795](https://github.com/element-hq/element-x-android/issues/2795))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix session verification being asked again for already verified users. ([#2718](https://github.com/element-hq/element-x-android/issues/2718))
|
||||||
|
- Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen. ([#2740](https://github.com/element-hq/element-x-android/issues/2740))
|
||||||
|
- Adjust the typography used in the selected user component so a user's display name fits better. ([#2760](https://github.com/element-hq/element-x-android/issues/2760))
|
||||||
|
- User display name overflows in timeline messages when it's way too long. ([#2761](https://github.com/element-hq/element-x-android/issues/2761))
|
||||||
|
- Ensure the application open the room when a notification is clicked. ([#2778](https://github.com/element-hq/element-x-android/issues/2778))
|
||||||
|
- Enforce mandatory session verification only for new logins. ([#2810](https://github.com/element-hq/element-x-android/issues/2810))
|
||||||
|
- Make log less verbose, make sure we upload as many log files as possible before reaching the request size limit of the bug reporting service, discard older logs if they don't fit. ([#2825](https://github.com/element-hq/element-x-android/issues/2825))
|
||||||
|
- Remove 'Join' button in room directory search results. ([#2827](https://github.com/element-hq/element-x-android/issues/2827))
|
||||||
|
- Add missing `app_id` and `Version` properties to bug reports. ([#2829](https://github.com/element-hq/element-x-android/issues/2829))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- RoomMember screen: fallback to userProfile data, if the member is not a user of the room. ([#2721](https://github.com/element-hq/element-x-android/issues/2721))
|
||||||
|
- Migrate application data. ([#2749](https://github.com/element-hq/element-x-android/issues/2749))
|
||||||
|
- Let the SDK manage the file log cleanup, and keep one week of log. ([#2758](https://github.com/element-hq/element-x-android/issues/2758))
|
||||||
|
- UX cleanup: reorder options in the main settings screen. ([#2801](https://github.com/element-hq/element-x-android/issues/2801))
|
||||||
|
- Analytics: Add support to report current session verification and recovery state ([#2806](https://github.com/element-hq/element-x-android/issues/2806))
|
||||||
|
- UX cleanup: room details screen, add new CTA buttons for Invite and Call actions. ([#2814](https://github.com/element-hq/element-x-android/issues/2814))
|
||||||
|
- UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too. ([#2818](https://github.com/element-hq/element-x-android/issues/2818))
|
||||||
|
- Add room badges to room details screen. ([#2822](https://github.com/element-hq/element-x-android/issues/2822))
|
||||||
|
|
||||||
|
Security
|
||||||
|
-------------
|
||||||
|
- Bump the Rust SDK to `v0.2.18` to remediate [CVE-2024-34353 / GHSA-9ggc-845v-gcgv](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-9ggc-845v-gcgv).
|
||||||
|
|
||||||
Changes in Element X v0.4.10 (2024-04-17)
|
Changes in Element X v0.4.10 (2024-04-17)
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
|
|
@ -20,6 +81,10 @@ Changes in Element X v0.4.9 (2024-04-12)
|
||||||
|
|
||||||
- Synchronize Localazy Strings.
|
- Synchronize Localazy Strings.
|
||||||
|
|
||||||
|
Security
|
||||||
|
----------
|
||||||
|
- Fix crash while processing a room message containing a malformed pill.
|
||||||
|
|
||||||
Changes in Element X v0.4.8 (2024-04-10)
|
Changes in Element X v0.4.8 (2024-04-10)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import extension.allServicesImpl
|
||||||
import extension.gitBranchName
|
import extension.gitBranchName
|
||||||
import extension.gitRevision
|
import extension.gitRevision
|
||||||
import extension.koverDependencies
|
import extension.koverDependencies
|
||||||
|
import extension.locales
|
||||||
import extension.setupKover
|
import extension.setupKover
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -74,6 +75,10 @@ android {
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
resourceConfigurations += locales
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:localeConfig="@xml/locales_config"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|
@ -74,6 +75,51 @@
|
||||||
|
|
||||||
<data android:scheme="io.element" />
|
<data android:scheme="io.element" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
Element web links
|
||||||
|
-->
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
|
||||||
|
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="app.element.io" />
|
||||||
|
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="develop.element.io" />
|
||||||
|
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
|
||||||
|
<data android:host="staging.element.io" />
|
||||||
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
matrix.to links
|
||||||
|
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
|
||||||
|
https://developer.android.com/training/app-links#web-links
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="matrix.to" />
|
||||||
|
</intent-filter>
|
||||||
|
<!--
|
||||||
|
links from matrix.to website
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="element" />
|
||||||
|
<data android:host="user" />
|
||||||
|
<data android:host="room" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
||||||
initializeComponent(TracingInitializer::class.java)
|
initializeComponent(TracingInitializer::class.java)
|
||||||
initializeComponent(CacheCleanerInitializer::class.java)
|
initializeComponent(CacheCleanerInitializer::class.java)
|
||||||
}
|
}
|
||||||
logApplicationInfo()
|
logApplicationInfo(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
|
|
@ -39,13 +42,16 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.compound.theme.isDark
|
import io.element.android.compound.theme.isDark
|
||||||
import io.element.android.compound.theme.mapToTheme
|
import io.element.android.compound.theme.mapToTheme
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.api.handleSecureFlag
|
import io.element.android.features.lockscreen.api.handleSecureFlag
|
||||||
import io.element.android.features.lockscreen.api.isLocked
|
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
import io.element.android.x.intent.SafeUriHandler
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MainActivity")
|
private val loggerTag = LoggerTag("MainActivity")
|
||||||
|
|
@ -59,27 +65,13 @@ class MainActivity : NodeActivity() {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
appBindings = bindings()
|
appBindings = bindings()
|
||||||
appBindings.lockScreenService().handleSecureFlag(this)
|
setupLockManagement(appBindings.lockScreenService(), appBindings.lockScreenEntryPoint())
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
MainContent(appBindings)
|
MainContent(appBindings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
override fun onBackPressed() {
|
|
||||||
// If the app is locked, we need to intercept onBackPressed before it goes to OnBackPressedDispatcher.
|
|
||||||
// Indeed, otherwise we would need to trick Appyx backstack management everywhere.
|
|
||||||
// Without this trick, we would get pop operations on the hidden backstack.
|
|
||||||
if (appBindings.lockScreenService().isLocked) {
|
|
||||||
// Do not kill the app in this case, just go to background.
|
|
||||||
moveTaskToBack(false)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainContent(appBindings: AppBindings) {
|
private fun MainContent(appBindings: AppBindings) {
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
|
|
@ -131,10 +123,27 @@ class MainActivity : NodeActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupLockManagement(
|
||||||
|
lockScreenService: LockScreenService,
|
||||||
|
lockScreenEntryPoint: LockScreenEntryPoint
|
||||||
|
) {
|
||||||
|
lockScreenService.handleSecureFlag(this)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
lockScreenService.lockState.collect { state ->
|
||||||
|
if (state == LockScreenLockState.Locked) {
|
||||||
|
startActivity(lockScreenEntryPoint.pinUnlockIntent(this@MainActivity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when:
|
* Called when:
|
||||||
* - the launcher icon is clicked (if the app is already running);
|
* - the launcher icon is clicked (if the app is already running);
|
||||||
* - a notification is clicked.
|
* - a notification is clicked.
|
||||||
|
* - a deep link have been clicked
|
||||||
* - the app is going to background (<- this is strange)
|
* - the app is going to background (<- this is strange)
|
||||||
*/
|
*/
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.api.MigrationEntryPoint
|
import io.element.android.features.api.MigrationEntryPoint
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
|
|
@ -38,4 +39,6 @@ interface AppBindings {
|
||||||
fun preferencesStore(): AppPreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
|
|
||||||
fun migrationEntryPoint(): MigrationEntryPoint
|
fun migrationEntryPoint(): MigrationEntryPoint
|
||||||
|
|
||||||
|
fun lockScreenEntryPoint(): LockScreenEntryPoint
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import dagger.Provides
|
||||||
import io.element.android.appconfig.ApplicationConfig
|
import io.element.android.appconfig.ApplicationConfig
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
|
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
||||||
|
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
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
@ -87,7 +88,7 @@ object AppModule {
|
||||||
// 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,
|
||||||
versionCode = BuildConfig.VERSION_CODE,
|
versionCode = context.getVersionCodeFromManifest(),
|
||||||
gitRevision = BuildConfig.GIT_REVISION,
|
gitRevision = BuildConfig.GIT_REVISION,
|
||||||
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
|
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
|
||||||
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
|
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.x.info
|
package io.element.android.x.info
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
||||||
import io.element.android.x.BuildConfig
|
import io.element.android.x.BuildConfig
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
fun logApplicationInfo() {
|
fun logApplicationInfo(context: Context) {
|
||||||
val appVersion = buildString {
|
val appVersion = buildString {
|
||||||
append(BuildConfig.VERSION_NAME)
|
append(BuildConfig.VERSION_NAME)
|
||||||
append(" (")
|
append(" (")
|
||||||
append(BuildConfig.VERSION_CODE)
|
append(context.getVersionCodeFromManifest())
|
||||||
append(") - ")
|
append(") - ")
|
||||||
append(BuildConfig.BUILD_TYPE)
|
append(BuildConfig.BUILD_TYPE)
|
||||||
append(" / ")
|
append(" / ")
|
||||||
|
|
|
||||||
17
app/src/main/res/resources.properties
Normal file
17
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 New Vector Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
unqualifiedResLocale=en
|
||||||
22
app/src/main/res/xml/locales_config.xml
Normal file
22
app/src/main/res/xml/locales_config.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit -->
|
||||||
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<locale android:name="be"/>
|
||||||
|
<locale android:name="bg"/>
|
||||||
|
<locale android:name="cs"/>
|
||||||
|
<locale android:name="de"/>
|
||||||
|
<locale android:name="en"/>
|
||||||
|
<locale android:name="es"/>
|
||||||
|
<locale android:name="fr"/>
|
||||||
|
<locale android:name="hu"/>
|
||||||
|
<locale android:name="in"/>
|
||||||
|
<locale android:name="it"/>
|
||||||
|
<locale android:name="ka"/>
|
||||||
|
<locale android:name="pt_BR"/>
|
||||||
|
<locale android:name="ro"/>
|
||||||
|
<locale android:name="ru"/>
|
||||||
|
<locale android:name="sk"/>
|
||||||
|
<locale android:name="sv"/>
|
||||||
|
<locale android:name="uk"/>
|
||||||
|
<locale android:name="zh-CN"/>
|
||||||
|
<locale android:name="zh-TW"/>
|
||||||
|
</locale-config>
|
||||||
|
|
@ -40,4 +40,9 @@ object ApplicationConfig {
|
||||||
* For Element, the value is "Element". We use the same name for desktop and mobile for now.
|
* For Element, the value is "Element". We use the same name for desktop and mobile for now.
|
||||||
*/
|
*/
|
||||||
const val DESKTOP_APPLICATION_NAME: String = "Element"
|
const val DESKTOP_APPLICATION_NAME: String = "Element"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum size of the upload request. Default value is just below CloudFlare's max request size.
|
||||||
|
*/
|
||||||
|
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appconfig
|
||||||
|
|
||||||
|
object MessageComposerConfig {
|
||||||
|
/**
|
||||||
|
* Enable the rich text editing in the composer.
|
||||||
|
*/
|
||||||
|
const val ENABLE_RICH_TEXT_EDITING = true
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,9 @@ object NotificationConfig {
|
||||||
// TODO EAx Implement and set to true at some point
|
// TODO EAx Implement and set to true at some point
|
||||||
const val SUPPORT_MARK_AS_READ_ACTION = false
|
const val SUPPORT_MARK_AS_READ_ACTION = false
|
||||||
|
|
||||||
|
// TODO EAx Implement and set to true at some point
|
||||||
|
const val SUPPORT_JOIN_DECLINE_INVITE = false
|
||||||
|
|
||||||
// TODO EAx Implement and set to true at some point
|
// TODO EAx Implement and set to true at some point
|
||||||
const val SUPPORT_QUICK_REPLY_ACTION = false
|
const val SUPPORT_QUICK_REPLY_ACTION = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ dependencies {
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
testImplementation(projects.features.rageshake.impl)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(libs.test.appyx.junit)
|
testImplementation(libs.test.appyx.junit)
|
||||||
testImplementation(libs.test.arch.core)
|
testImplementation(libs.test.arch.core)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.loggedin.LoggedInNode
|
import io.element.android.appnav.loggedin.LoggedInNode
|
||||||
import io.element.android.appnav.room.RoomFlowNode
|
import io.element.android.appnav.room.RoomFlowNode
|
||||||
|
|
@ -47,9 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
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.lockscreen.api.LockScreenEntryPoint
|
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
|
||||||
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
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
|
|
@ -57,16 +54,20 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
|
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.architecture.waitForNavTargetAttached
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
|
@ -91,12 +92,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||||
|
private val userProfileEntryPoint: UserProfileEntryPoint,
|
||||||
private val ftueEntryPoint: FtueEntryPoint,
|
private val ftueEntryPoint: FtueEntryPoint,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
|
||||||
private val lockScreenStateService: LockScreenService,
|
|
||||||
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
|
@ -106,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
permanentNavModel = PermanentNavModel(
|
permanentNavModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent),
|
navTargets = setOf(NavTarget.LoggedInPermanent),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -184,19 +184,23 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LoggedInPermanent : NavTarget
|
data object LoggedInPermanent : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object LockPermanent : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomList : NavTarget
|
data object RoomList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
val roomIdOrAlias: RoomIdOrAlias,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
val serverNames: List<String> = emptyList(),
|
||||||
|
val trigger: JoinedRoom.Trigger? = null,
|
||||||
val roomDescription: RoomDescription? = null,
|
val roomDescription: RoomDescription? = null,
|
||||||
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
|
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class UserProfile(
|
||||||
|
val userId: UserId,
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Settings(
|
data class Settings(
|
||||||
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
|
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
|
||||||
|
|
@ -223,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.LoggedInPermanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
createNode<LoggedInNode>(buildContext)
|
createNode<LoggedInNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.LockPermanent -> {
|
|
||||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.target(LockScreenEntryPoint.Target.Unlock)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
NavTarget.RoomList -> {
|
NavTarget.RoomList -> {
|
||||||
val callback = object : RoomListEntryPoint.Callback {
|
val callback = object : RoomListEntryPoint.Callback {
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
|
|
@ -270,21 +269,22 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
coroutineScope.launch { attachRoom(roomId) }
|
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPermalinkClicked(data: PermalinkData) {
|
override fun onPermalinkClicked(data: PermalinkData) {
|
||||||
when (data) {
|
when (data) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
// FIXME Add a user profile screen.
|
// Should not happen (handled by MessagesNode)
|
||||||
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
|
Timber.e("User link clicked: ${data.userId}.")
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.Room(
|
NavTarget.Room(
|
||||||
roomIdOrAlias = data.roomIdOrAlias,
|
roomIdOrAlias = data.roomIdOrAlias,
|
||||||
|
serverNames = data.viaParameters,
|
||||||
|
trigger = JoinedRoom.Trigger.Timeline,
|
||||||
initialElement = RoomNavigationTarget.Messages(data.eventId),
|
initialElement = RoomNavigationTarget.Messages(data.eventId),
|
||||||
// TODO Use the viaParameters
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -302,10 +302,23 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
val inputs = RoomFlowNode.Inputs(
|
val inputs = RoomFlowNode.Inputs(
|
||||||
roomIdOrAlias = navTarget.roomIdOrAlias,
|
roomIdOrAlias = navTarget.roomIdOrAlias,
|
||||||
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
roomDescription = Optional.ofNullable(navTarget.roomDescription),
|
||||||
|
serverNames = navTarget.serverNames,
|
||||||
|
trigger = Optional.ofNullable(navTarget.trigger),
|
||||||
initialElement = navTarget.initialElement
|
initialElement = navTarget.initialElement
|
||||||
)
|
)
|
||||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
|
is NavTarget.UserProfile -> {
|
||||||
|
val callback = object : UserProfileEntryPoint.Callback {
|
||||||
|
override fun onOpenRoom(roomId: RoomId) {
|
||||||
|
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userProfileEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
is NavTarget.Settings -> {
|
is NavTarget.Settings -> {
|
||||||
val callback = object : PreferencesEntryPoint.Callback {
|
val callback = object : PreferencesEntryPoint.Callback {
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
|
|
@ -321,7 +334,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||||
return preferencesEntryPoint.nodeBuilder(this, buildContext)
|
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(inputs)
|
.params(inputs)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -345,22 +358,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
NavTarget.Ftue -> {
|
NavTarget.Ftue -> {
|
||||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : FtueEntryPoint.Callback {
|
|
||||||
override fun onFtueFlowFinished() {
|
|
||||||
lifecycleScope.launch { attachRoomList() }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.RoomDirectorySearch -> {
|
NavTarget.RoomDirectorySearch -> {
|
||||||
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.callback(object : RoomDirectoryEntryPoint.Callback {
|
.callback(object : RoomDirectoryEntryPoint.Callback {
|
||||||
override fun onRoomJoined(roomId: RoomId) {
|
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResultClicked(roomDescription: RoomDescription) {
|
override fun onResultClicked(roomDescription: RoomDescription) {
|
||||||
backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription))
|
backstack.push(
|
||||||
|
NavTarget.Room(
|
||||||
|
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
|
||||||
|
roomDescription = roomDescription,
|
||||||
|
trigger = JoinedRoom.Trigger.RoomDirectory,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -368,37 +378,50 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun attachRoomList() {
|
suspend fun attachRoom(
|
||||||
if (!canShowRoomList()) return
|
roomIdOrAlias: RoomIdOrAlias,
|
||||||
attachChild<Node> {
|
serverNames: List<String> = emptyList(),
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
trigger: JoinedRoom.Trigger? = null,
|
||||||
|
eventId: EventId? = null,
|
||||||
|
) {
|
||||||
|
waitForNavTargetAttached { navTarget ->
|
||||||
|
navTarget is NavTarget.RoomList
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun attachRoom(roomId: RoomId) {
|
|
||||||
if (!canShowRoomList()) return
|
|
||||||
attachChild<RoomFlowNode> {
|
attachChild<RoomFlowNode> {
|
||||||
backstack.singleTop(NavTarget.RoomList)
|
backstack.push(
|
||||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
NavTarget.Room(
|
||||||
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
serverNames = serverNames,
|
||||||
|
trigger = trigger,
|
||||||
|
initialElement = RoomNavigationTarget.Messages(
|
||||||
|
focusedEventId = eventId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canShowRoomList(): Boolean {
|
suspend fun attachUser(userId: UserId) {
|
||||||
return ftueService.state.value is FtueState.Complete
|
waitForNavTargetAttached { navTarget ->
|
||||||
|
navTarget is NavTarget.RoomList
|
||||||
|
}
|
||||||
|
attachChild<Node> {
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.UserProfile(
|
||||||
|
userId = userId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
val ftueState by ftueService.state.collectAsState()
|
||||||
val isFtueDisplayed by ftueService.state.collectAsState()
|
|
||||||
BackstackView()
|
BackstackView()
|
||||||
if (isFtueDisplayed is FtueState.Complete) {
|
if (ftueState is FtueState.Complete) {
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
}
|
}
|
||||||
if (lockScreenState == LockScreenLockState.Locked) {
|
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixClientsHolder
|
import io.element.android.appnav.di.MatrixClientsHolder
|
||||||
import io.element.android.appnav.intent.IntentResolver
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
|
|
@ -55,6 +56,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
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.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -279,17 +282,39 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
when (resolvedIntent) {
|
when (resolvedIntent) {
|
||||||
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
||||||
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||||
|
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun navigateTo(permalinkData: PermalinkData) {
|
||||||
|
Timber.d("Navigating to $permalinkData")
|
||||||
|
attachSession(null)
|
||||||
|
.apply {
|
||||||
|
when (permalinkData) {
|
||||||
|
is PermalinkData.FallbackLink -> Unit
|
||||||
|
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
attachRoom(
|
||||||
|
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||||
|
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||||
|
serverNames = permalinkData.viaParameters,
|
||||||
|
eventId = permalinkData.eventId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
attachUser(permalinkData.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
Timber.d("Navigating to $deeplinkData")
|
Timber.d("Navigating to $deeplinkData")
|
||||||
attachSession(deeplinkData.sessionId)
|
attachSession(deeplinkData.sessionId)
|
||||||
.attachSession()
|
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoomList()
|
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,10 +323,12 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
oidcActionFlow.post(oidcAction)
|
oidcActionFlow.post(oidcAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
// [sessionId] will be null for permalink.
|
||||||
|
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
|
||||||
// TODO handle multi-session
|
// TODO handle multi-session
|
||||||
return waitForChildAttached { navTarget ->
|
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
|
||||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
|
||||||
}
|
}
|
||||||
|
.attachSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface ResolvedIntent {
|
sealed interface ResolvedIntent {
|
||||||
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
|
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
|
||||||
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
||||||
|
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntentResolver @Inject constructor(
|
class IntentResolver @Inject constructor(
|
||||||
private val deeplinkParser: DeeplinkParser,
|
private val deeplinkParser: DeeplinkParser,
|
||||||
private val oidcIntentResolver: OidcIntentResolver
|
private val oidcIntentResolver: OidcIntentResolver,
|
||||||
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
fun resolve(intent: Intent): ResolvedIntent? {
|
fun resolve(intent: Intent): ResolvedIntent? {
|
||||||
if (intent.canBeIgnored()) return null
|
if (intent.canBeIgnored()) return null
|
||||||
|
|
||||||
|
// Coming from a notification?
|
||||||
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
||||||
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
||||||
|
|
||||||
|
// Coming during login using Oidc?
|
||||||
val oidcAction = oidcIntentResolver.resolve(intent)
|
val oidcAction = oidcIntentResolver.resolve(intent)
|
||||||
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
||||||
|
|
||||||
|
// External link clicked? (matrix.to, element.io, etc.)
|
||||||
|
val permalinkData = intent
|
||||||
|
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||||
|
?.dataString
|
||||||
|
?.let { permalinkParser.parse(it) }
|
||||||
|
?.takeIf { it !is PermalinkData.FallbackLink }
|
||||||
|
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
|
||||||
|
|
||||||
// Unknown intent
|
// Unknown intent
|
||||||
Timber.w("Unknown intent")
|
Timber.w("Unknown intent")
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
|
||||||
|
fun SessionVerifiedStatus.toAnalyticsUserPropertyValue(): UserProperties.VerificationState? {
|
||||||
|
return when (this) {
|
||||||
|
// we don't need to report transient states
|
||||||
|
SessionVerifiedStatus.Unknown -> null
|
||||||
|
SessionVerifiedStatus.NotVerified -> UserProperties.VerificationState.NotVerified
|
||||||
|
SessionVerifiedStatus.Verified -> UserProperties.VerificationState.Verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecoveryState.toAnalyticsUserPropertyValue(): UserProperties.RecoveryState? {
|
||||||
|
return when (this) {
|
||||||
|
RecoveryState.ENABLED -> UserProperties.RecoveryState.Enabled
|
||||||
|
RecoveryState.DISABLED -> UserProperties.RecoveryState.Disabled
|
||||||
|
RecoveryState.INCOMPLETE -> UserProperties.RecoveryState.Incomplete
|
||||||
|
// we don't need to report transient states
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun SessionVerifiedStatus.toAnalyticsStateChangeValue(): CryptoSessionStateChange.VerificationState? {
|
||||||
|
return when (this) {
|
||||||
|
// we don't need to report transient states
|
||||||
|
SessionVerifiedStatus.Unknown -> null
|
||||||
|
SessionVerifiedStatus.NotVerified -> CryptoSessionStateChange.VerificationState.NotVerified
|
||||||
|
SessionVerifiedStatus.Verified -> CryptoSessionStateChange.VerificationState.Verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecoveryState.toAnalyticsStateChangeValue(): CryptoSessionStateChange.RecoveryState? {
|
||||||
|
return when (this) {
|
||||||
|
RecoveryState.ENABLED -> CryptoSessionStateChange.RecoveryState.Enabled
|
||||||
|
RecoveryState.DISABLED -> CryptoSessionStateChange.RecoveryState.Disabled
|
||||||
|
RecoveryState.INCOMPLETE -> CryptoSessionStateChange.RecoveryState.Incomplete
|
||||||
|
// we don't need to report transient states
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,15 +22,21 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
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
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LoggedInPresenter @Inject constructor(
|
class LoggedInPresenter @Inject constructor(
|
||||||
|
|
@ -38,6 +44,8 @@ class LoggedInPresenter @Inject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val encryptionService: EncryptionService,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
|
|
@ -48,10 +56,26 @@ class LoggedInPresenter @Inject constructor(
|
||||||
LaunchedEffect(isVerified) {
|
LaunchedEffect(isVerified) {
|
||||||
if (isVerified) {
|
if (isVerified) {
|
||||||
// Ensure pusher is registered
|
// Ensure pusher is registered
|
||||||
// TODO Manually select push provider for now
|
val currentPushProvider = pushService.getCurrentPushProvider()
|
||||||
|
val result = if (currentPushProvider == null) {
|
||||||
|
// Register with the first available push provider
|
||||||
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||||
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
|
} else {
|
||||||
|
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient)
|
||||||
|
if (currentPushDistributor == null) {
|
||||||
|
// Register with the first available distributor
|
||||||
|
val distributor = currentPushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
pushService.registerWith(matrixClient, currentPushProvider, distributor)
|
||||||
|
} else {
|
||||||
|
// Re-register with the current distributor
|
||||||
|
pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.onFailure {
|
||||||
|
Timber.e(it, "Failed to register pusher")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,8 +86,36 @@ class LoggedInPresenter @Inject constructor(
|
||||||
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val verificationState by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||||
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
|
LaunchedEffect(verificationState, recoveryState) {
|
||||||
|
reportCryptoStatusToAnalytics(verificationState, recoveryState)
|
||||||
|
}
|
||||||
|
|
||||||
return LoggedInState(
|
return LoggedInState(
|
||||||
showSyncSpinner = showSyncSpinner,
|
showSyncSpinner = showSyncSpinner,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reportCryptoStatusToAnalytics(verificationState: SessionVerifiedStatus, recoveryState: RecoveryState) {
|
||||||
|
// Update first the user property, to store the current status for that posthog user
|
||||||
|
val userVerificationState = verificationState.toAnalyticsUserPropertyValue()
|
||||||
|
val userRecoveryState = recoveryState.toAnalyticsUserPropertyValue()
|
||||||
|
if (userRecoveryState != null && userVerificationState != null) {
|
||||||
|
// we want to report when both value are known (if one is unknown we wait until we have them both)
|
||||||
|
analyticsService.updateUserProperties(
|
||||||
|
UserProperties(
|
||||||
|
verificationState = userVerificationState,
|
||||||
|
recoveryState = userRecoveryState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also report when there is a change in the state, to be able to track the changes
|
||||||
|
val changeVerificationState = verificationState.toAnalyticsStateChangeValue()
|
||||||
|
val changeRecoveryState = recoveryState.toAnalyticsStateChangeValue()
|
||||||
|
if (changeVerificationState != null && changeRecoveryState != null) {
|
||||||
|
analyticsService.capture(CryptoSessionStateChange(changeRecoveryState, changeVerificationState))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
|
|
@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -83,6 +85,8 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val roomIdOrAlias: RoomIdOrAlias,
|
val roomIdOrAlias: RoomIdOrAlias,
|
||||||
val roomDescription: Optional<RoomDescription>,
|
val roomDescription: Optional<RoomDescription>,
|
||||||
|
val serverNames: List<String>,
|
||||||
|
val trigger: Optional<JoinedRoom.Trigger>,
|
||||||
val initialElement: RoomNavigationTarget,
|
val initialElement: RoomNavigationTarget,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
|
|
@ -96,7 +100,11 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
data class Resolving(val roomAlias: RoomAlias) : NavTarget
|
data class Resolving(val roomAlias: RoomAlias) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class JoinRoom(val roomId: RoomId) : NavTarget
|
data class JoinRoom(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val serverNames: List<String>,
|
||||||
|
val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger,
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||||
|
|
@ -114,13 +122,13 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
backstack.newRoot(NavTarget.Resolving(i.roomAlias))
|
backstack.newRoot(NavTarget.Resolving(i.roomAlias))
|
||||||
}
|
}
|
||||||
is RoomIdOrAlias.Id -> {
|
is RoomIdOrAlias.Id -> {
|
||||||
subscribeToRoomInfoFlow(i.roomId)
|
subscribeToRoomInfoFlow(i.roomId, inputs.serverNames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeToRoomInfoFlow(roomId: RoomId) {
|
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
|
||||||
val roomInfoFlow = client.getRoomInfoFlow(
|
val roomInfoFlow = client.getRoomInfoFlow(
|
||||||
roomId = roomId
|
roomId = roomId
|
||||||
).map { it.getOrNull() }
|
).map { it.getOrNull() }
|
||||||
|
|
@ -136,7 +144,13 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
// we can have a space here in case the space has just been joined.
|
// we can have a space here in case the space has just been joined.
|
||||||
// So navigate to the JoinRoom target for now, which will
|
// So navigate to the JoinRoom target for now, which will
|
||||||
// handle the space not supported screen
|
// handle the space not supported screen
|
||||||
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
backstack.newRoot(
|
||||||
|
NavTarget.JoinRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
serverNames = serverNames,
|
||||||
|
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +161,13 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Was invited or the room is not known, display the join room screen
|
// Was invited or the room is not known, display the join room screen
|
||||||
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
backstack.newRoot(
|
||||||
|
NavTarget.JoinRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
serverNames = serverNames,
|
||||||
|
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.launchIn(lifecycleScope)
|
}.launchIn(lifecycleScope)
|
||||||
|
|
@ -158,8 +178,11 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
is NavTarget.Loading -> loadingNode(buildContext)
|
is NavTarget.Loading -> loadingNode(buildContext)
|
||||||
is NavTarget.Resolving -> {
|
is NavTarget.Resolving -> {
|
||||||
val callback = object : RoomAliasResolverEntryPoint.Callback {
|
val callback = object : RoomAliasResolverEntryPoint.Callback {
|
||||||
override fun onAliasResolved(roomId: RoomId) {
|
override fun onAliasResolved(data: ResolvedRoomAlias) {
|
||||||
subscribeToRoomInfoFlow(roomId)
|
subscribeToRoomInfoFlow(
|
||||||
|
roomId = data.roomId,
|
||||||
|
serverNames = data.servers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
|
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias)
|
||||||
|
|
@ -173,6 +196,8 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
roomId = navTarget.roomId,
|
roomId = navTarget.roomId,
|
||||||
roomIdOrAlias = inputs.roomIdOrAlias,
|
roomIdOrAlias = inputs.roomIdOrAlias,
|
||||||
roomDescription = inputs.roomDescription,
|
roomDescription = inputs.roomDescription,
|
||||||
|
serverNames = navTarget.serverNames,
|
||||||
|
trigger = navTarget.trigger,
|
||||||
)
|
)
|
||||||
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
joinRoomEntryPoint.createNode(this, buildContext, inputs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,21 @@ 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.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.FakeAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class MatrixClientsHolderTest {
|
class MatrixClientsHolderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test getOrNull`() {
|
fun `test getOrNull`() {
|
||||||
val fakeAuthenticationService = FakeAuthenticationService()
|
val fakeAuthenticationService = FakeMatrixAuthenticationService()
|
||||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test getOrRestore`() = runTest {
|
fun `test getOrRestore`() = runTest {
|
||||||
val fakeAuthenticationService = FakeAuthenticationService()
|
val fakeAuthenticationService = FakeMatrixAuthenticationService()
|
||||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
val fakeMatrixClient = FakeMatrixClient()
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
|
@ -47,7 +47,7 @@ class MatrixClientsHolderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test remove`() = runTest {
|
fun `test remove`() = runTest {
|
||||||
val fakeAuthenticationService = FakeAuthenticationService()
|
val fakeAuthenticationService = FakeMatrixAuthenticationService()
|
||||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
val fakeMatrixClient = FakeMatrixClient()
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
|
@ -60,7 +60,7 @@ class MatrixClientsHolderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test remove all`() = runTest {
|
fun `test remove all`() = runTest {
|
||||||
val fakeAuthenticationService = FakeAuthenticationService()
|
val fakeAuthenticationService = FakeMatrixAuthenticationService()
|
||||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
val fakeMatrixClient = FakeMatrixClient()
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
|
@ -73,7 +73,7 @@ class MatrixClientsHolderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test save and restore`() = runTest {
|
fun `test save and restore`() = runTest {
|
||||||
val fakeAuthenticationService = FakeAuthenticationService()
|
val fakeAuthenticationService = FakeMatrixAuthenticationService()
|
||||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
val fakeMatrixClient = FakeMatrixClient()
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.appnav.intent
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.api.oidc.OidcAction
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
|
@ -26,9 +27,13 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser
|
||||||
import io.element.android.libraries.deeplink.DeepLinkCreator
|
import io.element.android.libraries.deeplink.DeepLinkCreator
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
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.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -162,9 +167,60 @@ class IntentResolverTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink`() {
|
||||||
|
val permalinkData = PermalinkData.UserLink(
|
||||||
|
userId = UserId("@alice:matrix.org")
|
||||||
|
)
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { permalinkData }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = "https://matrix.to/#/@alice:matrix.org".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
ResolvedIntent.Permalink(
|
||||||
|
permalinkData = permalinkData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink, FallbackLink should be ignored`() {
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = "https://matrix.to/#/@alice:matrix.org".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve external permalink, invalid action`() {
|
||||||
|
val permalinkData = PermalinkData.UserLink(
|
||||||
|
userId = UserId("@alice:matrix.org")
|
||||||
|
)
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { permalinkData }
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
data = "https://matrix.to/invalid".toUri()
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve invalid`() {
|
fun `test resolve invalid`() {
|
||||||
val sut = createIntentResolver()
|
val sut = createIntentResolver(
|
||||||
|
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
|
||||||
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
data = "io.element:/invalid".toUri()
|
data = "io.element:/invalid".toUri()
|
||||||
|
|
@ -173,12 +229,17 @@ class IntentResolverTest {
|
||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createIntentResolver(): IntentResolver {
|
private fun createIntentResolver(
|
||||||
|
permalinkParserResult: () -> PermalinkData = { lambdaError() }
|
||||||
|
): IntentResolver {
|
||||||
return IntentResolver(
|
return IntentResolver(
|
||||||
deeplinkParser = DeeplinkParser(),
|
deeplinkParser = DeeplinkParser(),
|
||||||
oidcIntentResolver = DefaultOidcIntentResolver(
|
oidcIntentResolver = DefaultOidcIntentResolver(
|
||||||
oidcUrlParser = OidcUrlParser()
|
oidcUrlParser = OidcUrlParser()
|
||||||
),
|
),
|
||||||
|
permalinkParser = FakePermalinkParser(
|
||||||
|
result = permalinkParserResult
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AnalyticsVerificationStateMappingTests {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test verification Mappings`() = runTest {
|
||||||
|
assertThat(SessionVerifiedStatus.Verified.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.VerificationState.Verified)
|
||||||
|
assertThat(SessionVerifiedStatus.NotVerified.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.VerificationState.NotVerified)
|
||||||
|
|
||||||
|
assertThat(SessionVerifiedStatus.Verified.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.VerificationState.Verified)
|
||||||
|
assertThat(SessionVerifiedStatus.NotVerified.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.VerificationState.NotVerified)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test recovery state Mappings`() = runTest {
|
||||||
|
assertThat(RecoveryState.UNKNOWN.toAnalyticsUserPropertyValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.WAITING_FOR_SYNC.toAnalyticsUserPropertyValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.INCOMPLETE.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Incomplete)
|
||||||
|
assertThat(RecoveryState.ENABLED.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Enabled)
|
||||||
|
assertThat(RecoveryState.DISABLED.toAnalyticsUserPropertyValue())
|
||||||
|
.isEqualTo(UserProperties.RecoveryState.Disabled)
|
||||||
|
|
||||||
|
assertThat(RecoveryState.UNKNOWN.toAnalyticsStateChangeValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.WAITING_FOR_SYNC.toAnalyticsStateChangeValue())
|
||||||
|
.isNull()
|
||||||
|
assertThat(RecoveryState.INCOMPLETE.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Incomplete)
|
||||||
|
assertThat(RecoveryState.ENABLED.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Enabled)
|
||||||
|
assertThat(RecoveryState.DISABLED.toAnalyticsStateChangeValue())
|
||||||
|
.isEqualTo(CryptoSessionStateChange.RecoveryState.Disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,13 +20,19 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
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 kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -64,15 +70,56 @@ class LoggedInPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - report crypto status analytics`() = runTest {
|
||||||
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val verificationService = FakeSessionVerificationService()
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val presenter = LoggedInPresenter(
|
||||||
|
matrixClient = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService),
|
||||||
|
networkMonitor = FakeNetworkMonitor(NetworkStatus.Online),
|
||||||
|
pushService = FakePushService(),
|
||||||
|
sessionVerificationService = verificationService,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
encryptionService = encryptionService
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
|
verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
|
|
||||||
|
skipItems(4)
|
||||||
|
|
||||||
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedEvents[0]).isInstanceOf(CryptoSessionStateChange::class.java)
|
||||||
|
|
||||||
|
assertThat(analyticsService.capturedUserProperties.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedUserProperties[0].recoveryState).isEqualTo(UserProperties.RecoveryState.Incomplete)
|
||||||
|
assertThat(analyticsService.capturedUserProperties[0].verificationState).isEqualTo(UserProperties.VerificationState.Verified)
|
||||||
|
|
||||||
|
// ensure a sync status change does not trigger a new capture
|
||||||
|
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLoggedInPresenter(
|
private fun createLoggedInPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
networkStatus: NetworkStatus = NetworkStatus.Offline
|
networkStatus: NetworkStatus = NetworkStatus.Offline,
|
||||||
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||||
pushService = FakePushService(),
|
pushService = FakePushService(),
|
||||||
sessionVerificationService = FakeSessionVerificationService(),
|
sessionVerificationService = FakeSessionVerificationService(),
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
encryptionService = encryptionService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.3.12")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.3.13")
|
||||||
}
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix session verification being asked again for already verified users.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
RoomMember screen: fallback to userProfile data, if the member is not a user of the room.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Migrate application data.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Add support for expected decryption errors due to membership (UX and analytics).
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Let the SDK manage the file log cleanup, and keep one week of log.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Handle permalink navigation to Events.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Adjust the typography used in the selected user component so a user's display name fits better.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
User display name overflows in timeline messages when it's way too long.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Pretty-print event JSON in debug viewer
|
|
||||||
1
changelog.d/2809.bugfix
Normal file
1
changelog.d/2809.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Render selected/deselected room list filters on top
|
||||||
1
changelog.d/2893.misc
Normal file
1
changelog.d/2893.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
BugReporting | Add public device keys to rageshakes
|
||||||
1
changelog.d/2896.bugfix
Normal file
1
changelog.d/2896.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Set auto captilization, multiline and autocompletion flags for the markdown EditText.
|
||||||
1
changelog.d/2898.bugfix
Normal file
1
changelog.d/2898.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Restoree Markdown text input contents when returning to the room screen.
|
||||||
1
changelog.d/2912.misc
Normal file
1
changelog.d/2912.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Move push provider setting to the "Notifications" screen and display it only when several push provider are available.
|
||||||
1
changelog.d/2917.bugfix
Normal file
1
changelog.d/2917.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed sending rich content from android keyboards on the markdown text input
|
||||||
3
changelog.d/2924.misc
Normal file
3
changelog.d/2924.misc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Simplify notifications by removing the custom persistence layer.
|
||||||
|
|
||||||
|
Bump minSdk to 24 (Android 7).
|
||||||
1
changelog.d/2930.misc
Normal file
1
changelog.d/2930.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add a feature flag ShowBlockedUsersDetails, disabled by default to render display name and avatar of blocked users in the blocked users list.
|
||||||
71
docs/deeplink.md
Normal file
71
docs/deeplink.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Element X Android deeplink
|
||||||
|
|
||||||
|
<!--- TOC -->
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Asset Links](#asset-links)
|
||||||
|
* [Supported links](#supported-links)
|
||||||
|
* [Developer tools](#developer-tools)
|
||||||
|
|
||||||
|
<!--- END -->
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Element X Android supports deep linking to specific screens in the application. This document explains how to use deep links in Element X Android.
|
||||||
|
|
||||||
|
### Asset Links
|
||||||
|
|
||||||
|
The asset links file is available at https://element.io/.well-known/assetlinks.json
|
||||||
|
|
||||||
|
### Supported links
|
||||||
|
|
||||||
|
Element Call link:
|
||||||
|
> https://call.element.io/Example
|
||||||
|
|
||||||
|
Link to a user:
|
||||||
|
> https://app.element.io/#/user/@alice:matrix.org
|
||||||
|
|
||||||
|
Link to a room by id or alias:
|
||||||
|
> https://app.element.io/#/room/!roomid:matrix.org
|
||||||
|
> https://app.element.io/#/room/#element-x-android:matrix.org
|
||||||
|
|
||||||
|
Link to a room with a specific event:
|
||||||
|
> https://app.element.io/#/room/!roomid:matrix.org/$eventid
|
||||||
|
|
||||||
|
Note that it will also work with other domain such as:
|
||||||
|
> https://mobile.element.io
|
||||||
|
> https://develop.element.io
|
||||||
|
> https://staging.element.io
|
||||||
|
|
||||||
|
## Developer tools
|
||||||
|
|
||||||
|
Using an Android 12 or higher emulator
|
||||||
|
|
||||||
|
Ensure links verification is enabled
|
||||||
|
```bash
|
||||||
|
adb shell am compat enable 175408749 io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset link verifications for the given package id
|
||||||
|
```bash
|
||||||
|
adb shell pm set-app-links --package io.element.android.x.debug 0 all
|
||||||
|
```
|
||||||
|
|
||||||
|
Force the package id links to be verified
|
||||||
|
```bash
|
||||||
|
adb shell pm verify-app-links --re-verify io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the link verification of the package id
|
||||||
|
```bash
|
||||||
|
adb shell pm get-app-links io.element.android.x.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
io.element.android.x.debug:
|
||||||
|
ID: e2ece472-c266-4bf0-829c-be79959a6270
|
||||||
|
Signatures: [B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E]
|
||||||
|
Domain verification state:
|
||||||
|
*.element.io: 1024
|
||||||
|
```
|
||||||
10
fastlane/metadata/android/en-US/changelogs/40004120.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/40004120.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Main changes in this version:
|
||||||
|
|
||||||
|
- Added support for opening matrix URLs inside the app and navigating to replied to messages.
|
||||||
|
- Added per-app language support for Android 13+.
|
||||||
|
- Session verification is no longer mandatory for already logged in users.
|
||||||
|
- Better log handling.
|
||||||
|
- Fixed CVE-2024-34353 / GHSA-9ggc-845v-gcgv.
|
||||||
|
- UX improvements.
|
||||||
|
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40004130.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40004130.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Add plain text editor based on Markdown input.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -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_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s ."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Podes ler todos os nossos termos %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"Partilhar dados de utilização"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s ."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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">"Não recolheremos ou analisaremos quaisquer dados pessoais"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"Podes desligar qualquer momento"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -16,8 +16,12 @@
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.microphone"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
@ -28,24 +32,27 @@
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ElementCallActivity"
|
android:name=".ui.ElementCallActivity"
|
||||||
android:label="@string/element_call"
|
|
||||||
android:exported="true"
|
|
||||||
android:taskAffinity="io.element.android.features.call"
|
|
||||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
android:launchMode="singleTask">
|
android:exported="true"
|
||||||
|
android:label="@string/element_call"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="io.element.android.features.call">
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Matching asset file: https://call.element.io/.well-known/assetlinks.json -->
|
||||||
<data android:host="call.element.io" />
|
<data android:host="call.element.io" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
|
@ -55,6 +62,7 @@
|
||||||
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
|
@ -62,7 +70,10 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
|
<service
|
||||||
|
android:name=".CallForegroundService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,10 @@ class CallForegroundService : Service() {
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
} else {
|
|
||||||
stopForeground(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
|
|
||||||
6
features/call/src/main/res/values-ka/translations.xml
Normal file
6
features/call/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"მიმდინარე ზარი"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"დააწკაპუნეთ ზარში დასაბრუნებლად"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ ზარი მიმდინარეობს"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</string>
|
||||||
|
</resources>
|
||||||
6
features/call/src/main/res/values-zh/translations.xml
Normal file
6
features/call/src/main/res/values-zh/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"通话进行中"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"点按即可返回通话"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ 通话中"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
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.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -68,7 +68,7 @@ class CallScreenPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest {
|
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest {
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
|
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
|
|
@ -91,7 +91,7 @@ class CallScreenPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - set message interceptor, send and receive messages`() = runTest {
|
fun `present - set message interceptor, send and receive messages`() = runTest {
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
widgetDriver = widgetDriver,
|
widgetDriver = widgetDriver,
|
||||||
|
|
@ -119,7 +119,7 @@ class CallScreenPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
widgetDriver = widgetDriver,
|
widgetDriver = widgetDriver,
|
||||||
|
|
@ -149,7 +149,7 @@ class CallScreenPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
widgetDriver = widgetDriver,
|
widgetDriver = widgetDriver,
|
||||||
|
|
@ -178,7 +178,7 @@ class CallScreenPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
|
|
@ -201,7 +201,7 @@ class CallScreenPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
|
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
val widgetDriver = FakeWidgetDriver()
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
val presenter = createCallScreenPresenter(
|
val presenter = createCallScreenPresenter(
|
||||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
|
|
@ -229,7 +229,7 @@ class CallScreenPresenterTest {
|
||||||
private fun TestScope.createCallScreenPresenter(
|
private fun TestScope.createCallScreenPresenter(
|
||||||
callType: CallType,
|
callType: CallType,
|
||||||
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
||||||
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
|
||||||
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -27,7 +26,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ class DefaultCallWidgetProviderTest {
|
||||||
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom().apply {
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||||
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
|
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -89,7 +89,7 @@ class DefaultCallWidgetProviderTest {
|
||||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom().apply {
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||||
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
|
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ package io.element.android.features.call.utils
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
|
|
||||||
class FakeCallWidgetProvider(
|
class FakeCallWidgetProvider(
|
||||||
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
private val widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
|
||||||
private val url: String = "https://call.element.io",
|
private val url: String = "https://call.element.io",
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
var getWidgetCalled = false
|
var getWidgetCalled = false
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,10 @@ import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
|
||||||
import androidx.compose.material.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
|
@ -63,9 +61,7 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||||
import io.element.android.libraries.permissions.api.PermissionsView
|
import io.element.android.libraries.permissions.api.PermissionsView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomView(
|
fun ConfigureRoomView(
|
||||||
state: ConfigureRoomState,
|
state: ConfigureRoomState,
|
||||||
|
|
@ -73,17 +69,12 @@ fun ConfigureRoomView(
|
||||||
onRoomCreated: (RoomId) -> Unit,
|
onRoomCreated: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
|
||||||
initialValue = ModalBottomSheetValue.Hidden,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun onAvatarClicked() {
|
fun onAvatarClicked() {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
coroutineScope.launch {
|
isAvatarActionsSheetVisible.value = true
|
||||||
itemActionsBottomSheetState.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -143,7 +134,8 @@ fun ConfigureRoomView(
|
||||||
|
|
||||||
AvatarActionBottomSheet(
|
AvatarActionBottomSheet(
|
||||||
actions = state.avatarActions,
|
actions = state.avatarActions,
|
||||||
modalBottomSheetState = itemActionsBottomSheetState,
|
isVisible = isAvatarActionsSheetVisible.value,
|
||||||
|
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
||||||
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
|
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
|
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Raum-Name"</string>
|
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||||
<string name="screen_create_room_title">"Raum erstellen"</string>
|
<string name="screen_create_room_title">"Raum erstellen"</string>
|
||||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||||
|
|
|
||||||
|
|
@ -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_create_room_action_create_room">"ახალი ოთახი"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"ხალხის მოწვევა"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"ოთახის შექმნისას შეცდომა მოხდა"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"ამ ოთახში შეტყობინებები დაშიფრულია. შემდგომ დაშიფვრის გამორთვა შეუძლებელია."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"კერძო ოთახი (მხოლოდ მოწვევა)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"შეტყობინებები არ არის დაშიფრული და ყველას შეუძლია მათი წაკითხვა. შეგიძლიათ ჩართოთ დაშიფვრა მოგვიანებით."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"საჯარო ოთახი (ნებისმიერი)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
|
||||||
|
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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_create_room_action_create_room">"Nova sala"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"As mensagens serão cifradas. Uma vez ativada, não é possível desativar a cifragem."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Sala privada (entrada apenas por convite)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"As mensagens não serão cifradas e qualquer um as poderá ler. É possível ativar a cifragem posteriormente."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Sala pública (entrada livre)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||||
|
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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_create_room_action_create_room">"新聊天室"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"邀请朋友"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"创建房间时出错"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"此聊天室中的消息已加密。加密无法禁用。"</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"私人房间(仅限受邀者)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"消息未加密,任何人都可以查看。你可以稍后启用加密。"</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"公共房间(任何人)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"房间名称"</string>
|
||||||
|
<string name="screen_create_room_title">"创建房间"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"主题(可选)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"在开始聊天时发生了错误"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -18,18 +18,12 @@ package io.element.android.features.ftue.api
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
interface FtueEntryPoint : FeatureEntryPoint {
|
interface FtueEntryPoint : FeatureEntryPoint {
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
fun callback(callback: Callback): NodeBuilder
|
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
|
||||||
fun onFtueFlowFinished()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,6 @@ class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint {
|
||||||
val plugins = ArrayList<Plugin>()
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
return object : FtueEntryPoint.NodeBuilder {
|
return object : FtueEntryPoint.NodeBuilder {
|
||||||
override fun callback(callback: FtueEntryPoint.Callback): FtueEntryPoint.NodeBuilder {
|
|
||||||
plugins += callback
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun build(): Node {
|
override fun build(): Node {
|
||||||
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
|
||||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||||
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
|
|
@ -86,8 +85,6 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
data object LockScreenSetup : NavTarget
|
data object LockScreenSetup : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
||||||
|
|
@ -135,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
lifecycleScope.launch { moveToNextStep() }
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.target(LockScreenEntryPoint.Target.Setup)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +153,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
FtueStep.LockscreenSetup -> {
|
FtueStep.LockscreenSetup -> {
|
||||||
backstack.newRoot(NavTarget.LockScreenSetup)
|
backstack.newRoot(NavTarget.LockScreenSetup)
|
||||||
}
|
}
|
||||||
null -> callback?.onFtueFlowFinished()
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,24 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
|
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Уваход быў адменены на іншай прыладзе."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Запыт на ўваход скасаваны"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Запыт на іншай прыладзе не быў прыняты."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Уваход адхілены"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Тэрмін уваходу скончыўся. Калі ласка, паспрабуйце яшчэ раз."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Уваход у сістэму не быў завершаны своечасова"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Ваша іншая прылада не падтрымлівае ўваход у %s з дапамогай QR-кода.
|
||||||
|
|
||||||
|
Паспрабуйце ўвайсці ў сістэму ўручную або адскануйце QR-код з дапамогай іншай прылады."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не падтрымліваецца"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ваш правайдар уліковага запісу не падтрымлівае %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не падтрымліваецца"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Гатовы да сканавання"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Выберыце %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Паказаць QR-код”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,24 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
|
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Přihlášení bylo na druhém zařízení zrušeno."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Žádost o přihlášení zrušena"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Požadavek na vašem druhém zařízení nebyl přijat."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Přihlášení odmítnuto"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Platnost přihlášení vypršela. Zkuste to prosím znovu."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Přihlášení nebylo dokončeno včas"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zařízení nepodporuje přihlášení k %su pomocí QR kódu.
|
||||||
|
|
||||||
|
Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízení."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód není podporován"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Váš poskytovatel účtu nepodporuje %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s není podporováno"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Připraveno ke skenování"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Vybrat %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Naskenujte QR kód pomocí tohoto zařízení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Zobrazit QR kód\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
|
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Scanne den QR-Code mit diesem Gerät"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,44 @@
|
||||||
<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_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
|
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
|
||||||
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
|
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Établissement de la connexion"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Établissement d’une connexion sécurisée"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Essayez de vous connecter à nouveau à l’aide du code QR au cas où il s’agirait d’un problème réseau"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Si vous rencontrez le même problème, essayez un autre réseau wifi ou utilisez vos données mobiles au lieu du wifi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Si cela ne fonctionne pas, connectez-vous manuellement"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion n’est pas sécurisée"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"La connexion a été annulée sur l’autre appareil."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"La demande sur l’autre appareil n’a pas été acceptée."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un code QR. Essayer de vous connecter manuellement, ou scanner le code QR avec un autre appareil."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"Code QR non supporté"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s n’est pas supporté"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Prêt à scanner"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Choisissez %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Scanner le code QR avec cet appareil"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Accéder aux paramètres de l’appareil photo"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Vous devez autoriser %1$s à utiliser la camera de votre appareil pour continuer."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Autoriser l’usage de la caméra pour scanner le code QR"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Recommencer"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Une erreur inattendue s’est produite. Veuillez réessayer."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"En attente de votre autre session"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Votre fournisseur de compte peut vous demander le code suivant pour vérifier la connexion."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Votre code de vérification"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
|
||||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||||
<string name="screen_welcome_bullet_3">"N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."</string>
|
<string name="screen_welcome_bullet_3">"N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."</string>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
|
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Készen áll a beolvasásra"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Válassza ezt: %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"„QR-kód megjelenítése”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,33 @@
|
||||||
<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_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
|
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
|
||||||
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
|
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Stabilendo la connessione"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E adesso?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prova ad accedere di nuovo con un codice QR nel caso si sia verificato un problema di rete."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se riscontri lo stesso problema, prova con un altra rete wifi o usa i dati mobili al posto del wifi."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se il problema persiste, accedi manualmente"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connessione non è sicura"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Ti verrà chiesto di inserire le due cifre mostrate su questo dispositivo."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Inserisci il numero qui sotto sull\'altro dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Apri %1$s su un dispositivo desktop"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Clicca sul tuo avatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Seleziona %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Collega un nuovo dispositivo\""</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Segui le istruzioni mostrate"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Apri %1$s su un altro dispositivo per ottenere il codice QR"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Usa il codice QR mostrato sull\'altro dispositivo."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Codice QR sbagliato"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Vai alle impostazioni della fotocamera"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Per continuare, è necessario fornire l\'autorizzazione a %1$s per utilizzare la fotocamera del dispositivo."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Consenti l\'accesso alla fotocamera per la scansione del codice QR"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Scansiona il codice QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Ricomincia"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Si è verificato un errore inatteso. Riprova."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"In attesa dell\'altro dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Il fornitore dell\'account potrebbe richiedere il seguente codice per verificare l\'accesso."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Il tuo codice di verifica"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
|
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
|
||||||
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
|
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>
|
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>
|
||||||
|
|
|
||||||
11
features/ftue/impl/src/main/res/values-ka/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_notification_optin_subtitle">"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."</string>
|
||||||
|
<string name="screen_notification_optin_title">"ყველა შეტყობინებაზე შეტყობინებების მიღება"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე."</string>
|
||||||
|
<string name="screen_welcome_button">"დავიწყოთ!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"აი, რა უნდა იცოდეთ:"</string>
|
||||||
|
<string name="screen_welcome_title">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_notification_optin_subtitle">"Podes alterar as tuas definições mais tarde."</string>
|
||||||
|
<string name="screen_notification_optin_title">"Permite as notificações e nunca percas uma mensagem"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"A estabelecer uma ligação segura"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Não foi possível estabelecer uma ligação segura com o novo dispositivo. Os teus outros dispositivos continuam seguros, não precisas de te preocupar com eles."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E agora?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Tenta iniciar sessão novamente com um código QR, caso se trate de um problema de rede"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se tiveres o mesmo problema, experimenta uma rede Wi-Fi diferente ou utiliza os teus dados móveis."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se isso não funcionar, inicia sessão manualmente"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Ligação insegura"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Insere o número abaixo no teu dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Pedido de início de sessão cancelado"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Pronto para ler"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Abre a %1$s num computador"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Carrega no teu avatar"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Código QR inválido"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Ir para as configurações da câmara"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Para continuar, tens que dar permissão à %1$s para aceder à câmara do teu dispositivo."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Permitir o acesso à câmara para ler o código QR"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Ler o código QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Começar de novo"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Ocorreu um erro inesperado. Tenta novamente."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"À espera do teu outro dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"O teu fornecedor de conta pode pedir o seguinte código para verificar o início de sessão."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"O teu código de verificação"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Chamadas, sondagens, pesquisa e mais funcionalidades vão ser adicionadas ao longo do ano."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"O histórico de mensagens em salas cifradas ainda não está disponível."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Gostaríamos de ouvir a tua opinião, diz-nos o que pensas através da página de configurações."</string>
|
||||||
|
<string name="screen_welcome_button">"Vamos lá!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Eis o que tens de saber:"</string>
|
||||||
|
<string name="screen_welcome_title">"Bem-vindo à %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -2,7 +2,34 @@
|
||||||
<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_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
|
<string name="screen_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
|
||||||
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
|
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"Se stabilește o conexiune securizată"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Încercați să vă conectați din nou cu un cod QR în cazul în care a fost o problemă de rețea."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Dacă întâmpinați aceeași problemă, încercați o altă rețea Wi-Fi sau utilizați datele mobile în loc de Wi-Fi."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Dacă nu funcționează, conectați-vă manual"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"Selectați %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Conectați un dispozitiv nou”"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"Scanați codul QR cu acest dispozitiv"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"Deschideți %1$s pe un alt dispozitiv pentru a obține codul QR"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Utilizați codul QR afișat pe celălalt dispozitiv."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Cod QR greșit"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Mergeți la setările camerei"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Trebuie să acordați permisiunea ca %1$s să folosească camera dispozitivului pentru a continua."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Permiteți accesul la cameră pentru a scana codul QR"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"Scanați codul QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Începeți din nou"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"A apărut o eroare neașteptată. Vă rugăm să încercați din nou."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"În așteptarea celuilalt dispozitiv"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Furnizorul dumneavoastră de cont poate solicita următorul cod pentru a verifica conectarea."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Codul dumneavoastră de verificare"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
|
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
|
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
|
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,33 @@
|
||||||
<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_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
||||||
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Установление соединения"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Вам будет предложено ввести две цифры, показанные ниже."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам нужно будет ввести две цифры, показанные на этом устройстве."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Введите номер на своем устройстве"</string>
|
<string name="screen_qr_code_login_device_code_title">"Введите показанный номер на своем другом устройстве"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Вход на другом устройстве был отменен."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Запрос не был принят на другом устройстве."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
|
||||||
|
|
||||||
|
Попробуйте войти вручную или отсканируйте QR-код на другом устройстве."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не поддерживается"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,24 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
|
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Prihlásenie bolo zrušené na druhom zariadení."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Žiadosť na vašom druhom zariadení nebola prijatá."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
|
||||||
|
|
||||||
|
Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariadenia."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód nie je podporovaný"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie je podporovaný"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Pripravené na skenovanie"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Vyberte %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Naskenujte QR kód pomocou tohto zariadenia"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"„Zobraziť QR kód“"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
||||||
|
|
|
||||||
38
features/ftue/impl/src/main/res/values-zh/translations.xml
Normal file
38
features/ftue/impl/src/main/res/values-zh/translations.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_notification_optin_subtitle">"您可以稍后更改设置。"</string>
|
||||||
|
<string name="screen_notification_optin_title">"允许通知,绝不错过任何消息"</string>
|
||||||
|
<string name="screen_qr_code_login_connecting_subtitle">"建立安全连接"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"现在怎么办?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"如果这是网络问题,请尝试使用二维码再次登录"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"如果你遇到同样的问题,请尝试使用不同的 WiFi 网络或使用你的移动数据代替 WiFi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"如果不起作用,请手动登录"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"连接不安全"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"您会被要求输入此设备上显示的两位数。"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"在您的其他设备上输入下面的数字"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_1">"在桌面设备上打开 %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_2">"点击你的头像"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3">"选择 %1$s"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_3_action">"「连接新设备」"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_item_4">"按照说明进行操作"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_title">"在另一台设备上打开 %1$s 以获取二维码"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_description">"使用其他设备上显示的二维码。"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"再试一次"</string>
|
||||||
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"二维码错误"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"转到摄像头设置"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"您需要授予 %1$s 使用设备摄像头的权限才能继续。"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"允许摄像头权限以扫描 QR 码"</string>
|
||||||
|
<string name="screen_qr_code_login_scanning_state_title">"扫描二维码"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"重新开始"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"发生了意外错误。请再试一次。"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"等着您的其他设备"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"您的账户提供商可能会要求您提供以下代码来验证登录。"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"您的验证码"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"今年晚些时候将增加通话、投票、搜索等功能。"</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"加密房间的消息历史记录尚不可用。"</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。"</string>
|
||||||
|
<string name="screen_welcome_button">"开始吧!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"以下是您需要了解的内容:"</string>
|
||||||
|
<string name="screen_welcome_title">"欢迎使用 %1$s"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -11,12 +11,24 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"The request on your other device was not accepted."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Sign in declined"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Sign in expired. Please try again."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"The sign in was not completed in time"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Your other device does not support signing in to %s with a QR code.
|
||||||
|
|
||||||
|
Try signing in manually, or scan the QR code with another device."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR code not supported"</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Your account provider does not support %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s not supported"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Scan the QR code with this device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
|
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue