Merge branch 'release/0.5.1' into main

This commit is contained in:
ganfra 2024-08-28 12:11:57 +02:00
commit 2e3bae047d
668 changed files with 12193 additions and 2199 deletions

View file

@ -7,7 +7,8 @@
"PR-Dependencies" "PR-Dependencies"
], ],
"ignoreDeps" : [ "ignoreDeps" : [
"string:app_name" "string:app_name",
"gradle"
], ],
"packageRules" : [ "packageRules" : [
{ {

View file

@ -36,7 +36,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay APK - name: Assemble debug Gplay APK

View file

@ -44,7 +44,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK - name: Assemble debug Gplay Enterprise APK

View file

@ -19,7 +19,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12

View file

@ -1,18 +1,26 @@
name: Update Gradle Wrapper name: Update Gradle Wrapper
on: on:
workflow_dispatch:
schedule: schedule:
- cron: "0 0 * * *" - cron: "0 0 * * *"
jobs: jobs:
update-gradle-wrapper: update-gradle-wrapper:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4
name: Use JDK 17
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Update Gradle Wrapper - name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1 uses: gradle-update/update-gradle-wrapper-action@v1
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
target-branch: develop target-branch: develop
labels: PR-Build

View file

@ -1,15 +0,0 @@
name: "Validate Gradle Wrapper"
on:
pull_request:
merge_group:
push:
branches: [ main, develop ]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time.
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v3

View file

@ -39,7 +39,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK - name: Assemble debug APK

View file

@ -27,7 +27,7 @@ jobs:
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: false cache-read-only: false
@ -67,7 +67,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis - name: Dependency analysis

View file

@ -52,7 +52,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
@ -90,7 +90,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests - name: Run Konsist tests
@ -130,7 +130,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug - name: Build Gplay Debug
@ -174,7 +174,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt - name: Run Detekt
@ -214,7 +214,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check - name: Run Ktlint check
@ -254,7 +254,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit - name: Run Knit

View file

@ -39,7 +39,7 @@ jobs:
java-version: '17' java-version: '17'
# Add gradle cache, this should speed up the process # Add gradle cache, this should speed up the process
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots

View file

@ -25,7 +25,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
- name: Create app bundle - name: Create app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -61,7 +61,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
- name: Create Enterprise app bundle - name: Create Enterprise app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@ -89,7 +89,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
- name: Create APKs - name: Create APKs
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}

View file

@ -33,7 +33,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug - name: Build Gplay Debug

View file

@ -18,7 +18,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12

View file

@ -52,7 +52,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v4
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}

2
.idea/kotlinc.xml generated
View file

@ -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.24" /> <option name="version" value="1.9.25" />
</component> </component>
</project> </project>

View file

@ -1,3 +1,80 @@
Changes in Element X v0.5.0 (2024-07-24)
=========================================
### 🙌 Improvements
* Add icon for "Mark as read" and "Mark as unread" actions. by @bmarty in https://github.com/element-hq/element-x-android/pull/3144
* Add support for Picture In Picture for Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/3159
* Set pin grace period to 2 minutes by @bmarty in https://github.com/element-hq/element-x-android/pull/3172
* Unify the way we decide whether a room is a DM or a group room by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3100
* Subscribe to `RoomListItems` in the visible range by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3169
* Improve pip and add feature flag. by @bmarty in https://github.com/element-hq/element-x-android/pull/3199
* Open Source licenses: add color for links. by @bmarty in https://github.com/element-hq/element-x-android/pull/3215
* Cancel ringing call notification on call cancellation by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3047
### 🐛 Bugfixes
* Fix `MainActionButton` layout for long texts by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3158
* Always follow the desired theme for Pin, Incoming Call and Element Call screens by @bmarty in https://github.com/element-hq/element-x-android/pull/3165
* Fix empty screen issue after clearing the cache by @bmarty in https://github.com/element-hq/element-x-android/pull/3163
* Restore intentional mentions in the markdown/plain text editor by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3193
* Fix crash in the room list after a forced log out in background by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3180
* Clear existing notification when a room is marked as read by @bmarty in https://github.com/element-hq/element-x-android/pull/3203
* Fix crash when Pin code screen is displayed by @bmarty in https://github.com/element-hq/element-x-android/pull/3205
* Fix pillification not working for non formatted message bodies by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3201
* Update grammar on Matrix Ids to be more spec compliant and render error instead of infinite loading in room member list screen by @bmarty in https://github.com/element-hq/element-x-android/pull/3206
* Reduce the risk of text truncation in buttons. by @bmarty in https://github.com/element-hq/element-x-android/pull/3209
* Ensure that the manual dark theme is rendering correctly regarding -night resource and keyboard by @bmarty in https://github.com/element-hq/element-x-android/pull/3216
* Fix rendering issue of SunsetPage in dark mode by @bmarty in https://github.com/element-hq/element-x-android/pull/3217
* Fix linkification not working for `Spanned` strings in text messages by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3233
* Edit : fallback to room.edit when timeline item is not found. by @ganfra in https://github.com/element-hq/element-x-android/pull/3239
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3156
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3192
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3232
### 🧱 Build
* Remove Showkase processor not found warning from Danger by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3148
* Set targetSDK to 34 by @bmarty in https://github.com/element-hq/element-x-android/pull/3149
* Add a local copy of `inplace-fix.py` and `fix-pg-map-id.py` by @bmarty in https://github.com/element-hq/element-x-android/pull/3167
* Only add private SSH keys and clone submodules in the original repo by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3225
* Fix CI for forks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3226
### Dependency upgrades
* Update dependency io.element.android:compound-android to v0.0.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3143
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.31 by @renovate in https://github.com/element-hq/element-x-android/pull/3145
* Update dependency com.squareup:kotlinpoet to v1.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3150
* Update dependency org.robolectric:robolectric to v4.13 by @renovate in https://github.com/element-hq/element-x-android/pull/3157
* Update plugin dependencycheck to v10.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3154
* Update wysiwyg to v2.37.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3162
* Update plugin sonarqube to v5.1.0.4882 by @renovate in https://github.com/element-hq/element-x-android/pull/3139
* Update dependency org.jsoup:jsoup to v1.18.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3171
* Update dependency com.google.firebase:firebase-bom to v33.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3178
* Update telephoto to v0.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3191
* Update dependency com.google.truth:truth to v1.4.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3187
* Update dependency com.squareup:kotlinpoet to v1.18.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3194
* Update dependency io.mockk:mockk to v1.13.12 by @renovate in https://github.com/element-hq/element-x-android/pull/3198
* Update dependency io.sentry:sentry-android to v7.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3200
* Update plugin dependencycheck to v10.0.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3204
* Update dependency gradle to v8.9 by @renovate in https://github.com/element-hq/element-x-android/pull/3177
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.32 by @renovate in https://github.com/element-hq/element-x-android/pull/3202
* Update coil to v2.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3210
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.33 by @renovate in https://github.com/element-hq/element-x-android/pull/3220
* Update wysiwyg to v2.37.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3218
* Update telephoto to v0.12.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3230
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.34 by @renovate in https://github.com/element-hq/element-x-android/pull/3237
### Others
* Reduce delay when selecting room list filters by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3160
* Add `--alignment-preserved true` when signing APK for F-Droid. by @bmarty in https://github.com/element-hq/element-x-android/pull/3161
* Ensure that all callback plugins are invoked. by @bmarty in https://github.com/element-hq/element-x-android/pull/3146
* Add generated screen to show open source licenses in Gplay variant by @bmarty in https://github.com/element-hq/element-x-android/pull/3207
* Performance : improve time to open a room. by @ganfra in https://github.com/element-hq/element-x-android/pull/3186
* Add logging to help debug forced logout issues by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3208
* Use the right filename for log files so they're sorted in rageshakes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3219
* Compose : add immutability to some Reaction classes by @ganfra in https://github.com/element-hq/element-x-android/pull/3224
* Fix stickers display text on room summary by @surakin in https://github.com/element-hq/element-x-android/pull/3221
* Rework FakeMatrixRoom so that it contains only lambdas. by @bmarty in https://github.com/element-hq/element-x-android/pull/3229
Changes in Element X v0.4.16 (2024-07-05) Changes in Element X v0.4.16 (2024-07-05)
========================================= =========================================

View file

@ -1,7 +1,7 @@
[![Latest build](https://github.com/element-hq/element-x-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/element-hq/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop) [![Latest build](https://github.com/element-hq/element-x-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/element-hq/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-x-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-x-android)
[![codecov](https://codecov.io/github/element-hq/element-x-android/branch/develop/graph/badge.svg?token=ecwvia7amV)](https://codecov.io/github/vector-im/element-x-android) [![codecov](https://codecov.io/github/element-hq/element-x-android/branch/develop/graph/badge.svg?token=ecwvia7amV)](https://codecov.io/github/vector-im/element-x-android)
[![Element X Android Matrix room #element-x-android:matrix.org](https://img.shields.io/matrix/element-x-android:matrix.org.svg?label=%23element-x-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-x-android:matrix.org) [![Element X Android Matrix room #element-x-android:matrix.org](https://img.shields.io/matrix/element-x-android:matrix.org.svg?label=%23element-x-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-x-android:matrix.org)
[![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element) [![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element)

View file

@ -40,3 +40,5 @@
-keepclassmembers class android.view.JavaViewSpy { -keepclassmembers class android.view.JavaViewSpy {
static int windowAttachCount(android.view.View); static int windowAttachCount(android.view.View);
} }
-keep class io.element.android.x.di.** { *; }

View file

@ -13,6 +13,7 @@
<locale android:name="in"/> <locale android:name="in"/>
<locale android:name="it"/> <locale android:name="it"/>
<locale android:name="ka"/> <locale android:name="ka"/>
<locale android:name="nl"/>
<locale android:name="pl"/> <locale android:name="pl"/>
<locale android:name="pt"/> <locale android:name="pt"/>
<locale android:name="pt_BR"/> <locale android:name="pt_BR"/>
@ -21,6 +22,7 @@
<locale android:name="sk"/> <locale android:name="sk"/>
<locale android:name="sv"/> <locale android:name="sv"/>
<locale android:name="uk"/> <locale android:name="uk"/>
<locale android:name="uz"/>
<locale android:name="zh-CN"/> <locale android:name="zh-CN"/>
<locale android:name="zh-TW"/> <locale android:name="zh-TW"/>
</locale-config> </locale-config>

View file

@ -31,7 +31,6 @@ object TimelineConfig {
StateEventType.ROOM_GUEST_ACCESS, StateEventType.ROOM_GUEST_ACCESS,
StateEventType.ROOM_HISTORY_VISIBILITY, StateEventType.ROOM_HISTORY_VISIBILITY,
StateEventType.ROOM_JOIN_RULES, StateEventType.ROOM_JOIN_RULES,
StateEventType.ROOM_PINNED_EVENTS,
StateEventType.ROOM_POWER_LEVELS, StateEventType.ROOM_POWER_LEVELS,
StateEventType.ROOM_SERVER_ACL, StateEventType.ROOM_SERVER_ACL,
StateEventType.ROOM_TOMBSTONE, StateEventType.ROOM_TOMBSTONE,

View file

@ -42,6 +42,7 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.deeplink) implementation(projects.libraries.deeplink)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oidc.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
@ -66,6 +67,7 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)

View file

@ -42,8 +42,6 @@ import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView import io.element.android.appnav.root.RootView
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcActionFlow
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.features.viewfolder.api.ViewFolderEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
@ -58,6 +56,8 @@ 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.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.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
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

View file

@ -17,12 +17,12 @@
package io.element.android.appnav.intent package io.element.android.appnav.intent
import android.content.Intent import android.content.Intent
import io.element.android.features.login.api.oidc.OidcAction
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.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcIntentResolver
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject

View file

@ -21,9 +21,6 @@ import android.content.Intent
import android.net.Uri 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.impl.oidc.DefaultOidcIntentResolver
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
@ -33,6 +30,9 @@ 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.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
import io.element.android.libraries.oidc.impl.OidcUrlParser
import io.element.android.tests.testutils.lambda.lambdaError 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

View file

@ -61,7 +61,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.4.5") detektPlugins("io.nlopez.compose.rules:detekt:0.4.10")
} }
// KtLint // KtLint
@ -129,11 +129,11 @@ dependencyAnalysis {
// To run a sonar analysis: // To run a sonar analysis:
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>' // Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma // The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android // Sonar result can be found here: https://sonarcloud.io/project/overview?id=element-x-android
sonar { sonar {
properties { properties {
property("sonar.projectName", "element-x-android") property("sonar.projectName", "element-x-android")
property("sonar.projectKey", "vector-im_element-x-android") property("sonar.projectKey", "element-x-android")
property("sonar.host.url", "https://sonarcloud.io") property("sonar.host.url", "https://sonarcloud.io")
property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName) property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
property("sonar.sourceEncoding", "UTF-8") property("sonar.sourceEncoding", "UTF-8")
@ -141,7 +141,7 @@ sonar {
property("sonar.links.ci", "https://github.com/element-hq/element-x-android/actions") property("sonar.links.ci", "https://github.com/element-hq/element-x-android/actions")
property("sonar.links.scm", "https://github.com/element-hq/element-x-android/") property("sonar.links.scm", "https://github.com/element-hq/element-x-android/")
property("sonar.links.issue", "https://github.com/element-hq/element-x-android/issues") property("sonar.links.issue", "https://github.com/element-hq/element-x-android/issues")
property("sonar.organization", "new_vector_ltd_organization") property("sonar.organization", "element-hq")
property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid") property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")
// exclude source code from analyses separated by a colon (:) // exclude source code from analyses separated by a colon (:)

View file

@ -0,0 +1,2 @@
Main changes in this version: Element Call improvements and bug fixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View 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="screen_analytics_settings_help_us_improve">"Deel anonieme gebruiksgegevens om ons te helpen problemen te identificeren."</string>
<string name="screen_analytics_settings_read_terms">"Je kunt al onze voorwaarden %1$s lezen."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
</resources>

View file

@ -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">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string> <string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_settings_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string> <string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string> <string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string> <string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
</resources> </resources>

View file

@ -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">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
<string name="screen_analytics_settings_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"Bu yerga"</string>
<string name="screen_analytics_settings_share_data">"Analitik ma\'lumotlarni ulashish"</string>
</resources>

View file

@ -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">"We zullen geen persoonlijke gegevens registreren of er een profiel van maken"</string>
<string name="screen_analytics_prompt_help_us_improve">"Deel anonieme gebruiksgegevens om ons te helpen problemen te identificeren."</string>
<string name="screen_analytics_prompt_read_terms">"Je kunt al onze voorwaarden %1$s lezen."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Je kunt dit op elk moment uitschakelen"</string>
<string name="screen_analytics_prompt_third_party_sharing">"We delen je gegevens niet met derden"</string>
<string name="screen_analytics_prompt_title">"Help %1$s te verbeteren"</string>
</resources>

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string> <string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string> <string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_prompt_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string> <string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string> <string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string> <string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string> <string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych stronom trzecim"</string>
<string name="screen_analytics_prompt_title">"Pomóż nam ulepszyć %1$s"</string> <string name="screen_analytics_prompt_title">"Pomóż nam ulepszyć %1$s"</string>
</resources> </resources>

View file

@ -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">"Biz hech qanday shaxsiy ma\'lumotlarni yozmaymiz yoki profilga kiritmaymiz"</string>
<string name="screen_analytics_prompt_help_us_improve">"Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."</string>
<string name="screen_analytics_prompt_read_terms">"Siz bizning barcha shartlarimizni o\'qishingiz mumkin%1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"Bu yerga"</string>
<string name="screen_analytics_prompt_settings">"Buni istalgan vaqtda oʻchirib qoʻyishingiz mumkin"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Biz sizning ma\'lumotlaringizni uchinchi tomonlar bilan baham ko\'rmaymiz"</string>
<string name="screen_analytics_prompt_title">"Yaxshilashga yordam bering%1$s"</string>
</resources>

View file

@ -4,7 +4,7 @@
<string name="screen_analytics_prompt_help_us_improve">"共享匿名使用数据以帮助我们排查问题。"</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">"您可以阅读我们的所有条款 %1$s。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"此处"</string> <string name="screen_analytics_prompt_read_terms_content_link">"此处"</string>
<string name="screen_analytics_prompt_settings">"可以随时关闭此功能"</string> <string name="screen_analytics_prompt_settings">"可以随时关闭此功能"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我们不会与第三方共享您的数据"</string> <string name="screen_analytics_prompt_third_party_sharing">"我们不会与第三方共享您的数据"</string>
<string name="screen_analytics_prompt_title">"帮助改进 %1$s"</string> <string name="screen_analytics_prompt_title">"帮助改进 %1$s"</string>
</resources> </resources>

View file

@ -16,6 +16,10 @@
package io.element.android.features.call.impl.pip package io.element.android.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController
sealed interface PictureInPictureEvents { sealed interface PictureInPictureEvents {
data class SetPipController(val pipController: PipController) : PictureInPictureEvents
data object EnterPictureInPicture : PictureInPictureEvents data object EnterPictureInPicture : PictureInPictureEvents
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvents
} }

View file

@ -16,17 +16,17 @@
package io.element.android.features.call.impl.pip package io.element.android.features.call.impl.pip
import android.app.Activity
import android.app.PictureInPictureParams
import android.os.Build
import android.util.Rational
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.call.impl.utils.PipController
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
private val loggerTag = LoggerTag("PiP") private val loggerTag = LoggerTag("PiP")
@ -35,71 +35,69 @@ class PictureInPicturePresenter @Inject constructor(
pipSupportProvider: PipSupportProvider, pipSupportProvider: PipSupportProvider,
) : Presenter<PictureInPictureState> { ) : Presenter<PictureInPictureState> {
private val isPipSupported = pipSupportProvider.isPipSupported() private val isPipSupported = pipSupportProvider.isPipSupported()
private var isInPictureInPicture = mutableStateOf(false) private var pipView: PipView? = null
private var hostActivity: WeakReference<Activity>? = null
@Composable @Composable
override fun present(): PictureInPictureState { override fun present(): PictureInPictureState {
val coroutineScope = rememberCoroutineScope()
var isInPictureInPicture by remember { mutableStateOf(false) }
var pipController by remember { mutableStateOf<PipController?>(null) }
fun handleEvent(event: PictureInPictureEvents) { fun handleEvent(event: PictureInPictureEvents) {
when (event) { when (event) {
PictureInPictureEvents.EnterPictureInPicture -> switchToPip() is PictureInPictureEvents.SetPipController -> {
pipController = event.pipController
}
PictureInPictureEvents.EnterPictureInPicture -> {
coroutineScope.launch {
switchToPip(pipController)
}
}
is PictureInPictureEvents.OnPictureInPictureModeChanged -> {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: ${event.isInPip}")
isInPictureInPicture = event.isInPip
if (event.isInPip) {
pipController?.enterPip()
} else {
pipController?.exitPip()
}
}
} }
} }
return PictureInPictureState( return PictureInPictureState(
supportPip = isPipSupported, supportPip = isPipSupported,
isInPictureInPicture = isInPictureInPicture.value, isInPictureInPicture = isInPictureInPicture,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
fun onCreate(activity: Activity) { fun setPipView(pipView: PipView?) {
if (isPipSupported) { if (isPipSupported) {
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params") Timber.tag(loggerTag.value).d("Setting PiP params")
hostActivity = WeakReference(activity) this.pipView = pipView
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams()) pipView?.setPipParams()
} else { } else {
Timber.tag(loggerTag.value).d("onCreate: PiP is not supported") Timber.tag(loggerTag.value).d("setPipView: PiP is not supported")
} }
} }
fun onDestroy() {
Timber.tag(loggerTag.value).d("onDestroy")
hostActivity?.clear()
hostActivity = null
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getPictureInPictureParams(): PictureInPictureParams {
return PictureInPictureParams.Builder()
// Portrait for calls seems more appropriate
.setAspectRatio(Rational(3, 5))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
}
}
.build()
}
/** /**
* Enters Picture-in-Picture mode. * Enters Picture-in-Picture mode, if allowed by Element Call.
*/ */
private fun switchToPip() { private suspend fun switchToPip(pipController: PipController?) {
if (isPipSupported) { if (isPipSupported) {
Timber.tag(loggerTag.value).d("Switch to PiP mode") if (pipController == null) {
hostActivity?.get()?.enterPictureInPictureMode(getPictureInPictureParams()) Timber.tag(loggerTag.value).w("webPipApi is not available")
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") } }
if (pipController == null || pipController.canEnterPip()) {
Timber.tag(loggerTag.value).d("Switch to PiP mode")
pipView?.enterPipMode()
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
} else {
Timber.tag(loggerTag.value).w("Cannot enter PiP mode, hangup the call")
pipView?.hangUp()
}
} }
} }
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
isInPictureInPicture.value = isInPictureInPictureMode
}
fun onUserLeaveHint() {
Timber.tag(loggerTag.value).d("onUserLeaveHint")
switchToPip()
}
} }

View file

@ -24,9 +24,6 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.runBlocking
import javax.inject.Inject import javax.inject.Inject
interface PipSupportProvider { interface PipSupportProvider {
@ -37,15 +34,10 @@ interface PipSupportProvider {
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPipSupportProvider @Inject constructor( class DefaultPipSupportProvider @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val featureFlagService: FeatureFlagService,
) : PipSupportProvider { ) : PipSupportProvider {
override fun isPipSupported(): Boolean { override fun isPipSupported(): Boolean {
val isSupportedByTheOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && val isSupportedByTheOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse() context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse()
return if (isSupportedByTheOs) { return isSupportedByTheOs
runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PictureInPicture) }
} else {
false
}
} }
} }

View file

@ -14,8 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.lockscreen.impl.unlock.signout package io.element.android.features.call.impl.pip
interface SignOut { interface PipView {
suspend operator fun invoke(): String? fun setPipParams()
fun enterPipMode(): Boolean
fun hangUp()
} }

View file

@ -40,6 +40,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
@ -95,9 +96,9 @@ internal fun CallScreenView(
} }
CallWebView( CallWebView(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.fillMaxSize(), .fillMaxSize(),
url = state.urlState, url = state.urlState,
userAgent = state.userAgent, userAgent = state.userAgent,
onPermissionsRequest = { request -> onPermissionsRequest = { request ->
@ -108,6 +109,8 @@ internal fun CallScreenView(
onWebViewCreate = { webView -> onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView) val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
} }
) )
when (state.urlState) { when (state.urlState) {

View file

@ -17,6 +17,7 @@
package io.element.android.features.call.impl.ui package io.element.android.features.call.impl.ui
import android.Manifest import android.Manifest
import android.app.PictureInPictureParams
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.media.AudioAttributes import android.media.AudioAttributes
@ -24,19 +25,30 @@ import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Rational
import android.view.WindowManager import android.view.WindowManager
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import io.element.android.features.call.api.CallType import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.DefaultElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPicturePresenter import io.element.android.features.call.impl.pip.PictureInPicturePresenter
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
@ -45,7 +57,10 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { class ElementCallActivity :
AppCompatActivity(),
CallScreenNavigator,
PipView {
@Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var appPreferencesStore: AppPreferencesStore
@ -86,13 +101,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
updateUiMode(resources.configuration) updateUiMode(resources.configuration)
} }
pictureInPicturePresenter.onCreate(this) pictureInPicturePresenter.setPipView(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
requestAudioFocus() requestAudioFocus()
setContent { setContent {
val pipState = pictureInPicturePresenter.present() val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
ElementThemeApp(appPreferencesStore) { ElementThemeApp(appPreferencesStore) {
val state = presenter.present() val state = presenter.present()
eventSink = state.eventSink eventSink = state.eventSink
@ -108,21 +124,38 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
} }
} }
@Composable
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
val pipEventSink by rememberUpdatedState(pipState.eventSink)
DisposableEffect(Unit) {
val onUserLeaveHintListener = Runnable {
pipEventSink(PictureInPictureEvents.EnterPictureInPicture)
}
addOnUserLeaveHintListener(onUserLeaveHintListener)
onDispose {
removeOnUserLeaveHintListener(onUserLeaveHintListener)
}
}
DisposableEffect(Unit) {
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode))
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.d("Exiting PiP mode: Hangup the call")
eventSink?.invoke(CallScreenEvents.Hangup)
}
}
addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener)
onDispose {
removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener)
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
updateUiMode(newConfig) updateUiMode(newConfig)
} }
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.d("Exiting PiP mode: Hangup the call")
eventSink?.invoke(CallScreenEvents.Hangup)
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setCallType(intent) setCallType(intent)
@ -140,16 +173,11 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
} }
} }
override fun onUserLeaveHint() {
super.onUserLeaveHint()
pictureInPicturePresenter.onUserLeaveHint()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
releaseAudioFocus() releaseAudioFocus()
CallForegroundService.stop(this) CallForegroundService.stop(this)
pictureInPicturePresenter.onDestroy() pictureInPicturePresenter.setPipView(null)
} }
override fun finish() { override fun finish() {
@ -249,6 +277,33 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
} }
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun setPipParams() {
setPictureInPictureParams(getPictureInPictureParams())
}
@RequiresApi(Build.VERSION_CODES.O)
override fun enterPipMode(): Boolean {
return enterPictureInPictureMode(getPictureInPictureParams())
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getPictureInPictureParams(): PictureInPictureParams {
return PictureInPictureParams.Builder()
// Portrait for calls seems more appropriate
.setAspectRatio(Rational(3, 5))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
}
}
.build()
}
override fun hangUp() {
eventSink?.invoke(CallScreenEvents.Hangup)
}
} }
internal fun mapWebkitPermissions(permissions: Array<String>): List<String> { internal fun mapWebkitPermissions(permissions: Array<String>): List<String> {

View file

@ -14,15 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.call.impl.utils
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut interface PipController {
import io.element.android.tests.testutils.simulateLongTask suspend fun canEnterPip(): Boolean
fun enterPip()
class FakeSignOut( fun exitPip()
var lambda: () -> String? = { null }
) : SignOut {
override suspend fun invoke(): String? = simulateLongTask {
lambda()
}
} }

View file

@ -0,0 +1,42 @@
/*
* 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.features.call.impl.utils
import android.webkit.WebView
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class WebViewPipController(
private val webView: WebView,
) : PipController {
override suspend fun canEnterPip(): Boolean {
return suspendCoroutine { continuation ->
webView.evaluateJavascript("controls.canEnterPip()") { result ->
// Note if the method is not available, it will return "null"
continuation.resume(result == "true" || result == "null")
}
}
}
override fun enterPip() {
webView.evaluateJavascript("controls.enablePip()", null)
}
override fun exitPip() {
webView.evaluateJavascript("controls.disablePip()", null)
}
}

View 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">"Actieve oproep"</string>
<string name="call_foreground_service_message_android">"Tik om terug te gaan naar het gesprek"</string>
<string name="call_foreground_service_title_android">"☎️ In gesprek"</string>
</resources>

View file

@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string> <string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string> <string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string> <string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
<string name="screen_incoming_call_subtitle_android">"Przychodzące połączenie Element"</string>
</resources> </resources>

View file

@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"Pågående samtal"</string> <string name="call_foreground_service_channel_title_android">"Pågående samtal"</string>
<string name="call_foreground_service_message_android">"Tryck för att återgå till samtalet"</string> <string name="call_foreground_service_message_android">"Tryck för att återgå till samtalet"</string>
<string name="call_foreground_service_title_android">"☎️ Samtal pågår"</string> <string name="call_foreground_service_title_android">"☎️ Samtal pågår"</string>
<string name="screen_incoming_call_subtitle_android">"Inkommande Element Call"</string>
</resources> </resources>

View 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">"Davom etayotgan qo\'ng\'iroq"</string>
<string name="call_foreground_service_message_android">"Qo\'ng\'iroqqa qaytish uchun bosing"</string>
<string name="call_foreground_service_title_android">"☎️ Qongiroq davom etmoqda"</string>
</resources>

View file

@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"通话进行中"</string> <string name="call_foreground_service_channel_title_android">"通话进行中"</string>
<string name="call_foreground_service_message_android">"点按即可返回通话"</string> <string name="call_foreground_service_message_android">"点按即可返回通话"</string>
<string name="call_foreground_service_title_android">"☎️ 通话中"</string> <string name="call_foreground_service_title_android">"☎️ 通话中"</string>
<string name="screen_incoming_call_subtitle_android">"Element 来电"</string>
</resources> </resources>

View file

@ -0,0 +1,32 @@
/*
* 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
*
* https://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.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController
import io.element.android.tests.testutils.lambda.lambdaError
class FakePipController(
private val canEnterPipResult: () -> Boolean = { lambdaError() },
private val enterPipResult: () -> Unit = { lambdaError() },
private val exitPipResult: () -> Unit = { lambdaError() },
) : PipController {
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
override fun enterPip() = enterPipResult()
override fun exitPip() = exitPipResult()
}

View file

@ -0,0 +1,29 @@
/*
* 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
*
* https://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.features.call.impl.pip
import io.element.android.tests.testutils.lambda.lambdaError
class FakePipView(
private val setPipParamsResult: () -> Unit = { lambdaError() },
private val enterPipModeResult: () -> Boolean = { lambdaError() },
private val handUpResult: () -> Unit = { lambdaError() }
) : PipView {
override fun setPipParams() = setPipParamsResult()
override fun enterPipMode(): Boolean = enterPipModeResult()
override fun hangUp() = handUpResult()
}

View file

@ -16,23 +16,16 @@
package io.element.android.features.call.impl.pip package io.element.android.features.call.impl.pip
import android.os.Build.VERSION_CODES
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.ui.ElementCallActivity import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
class PictureInPicturePresenterTest { class PictureInPicturePresenterTest {
@Test @Test
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
fun `when pip is not supported, the state value supportPip is false`() = runTest { fun `when pip is not supported, the state value supportPip is false`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = false) val presenter = createPictureInPicturePresenter(supportPip = false)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -41,68 +34,119 @@ class PictureInPicturePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.supportPip).isFalse() assertThat(initialState.supportPip).isFalse()
} }
presenter.onDestroy() presenter.setPipView(null)
} }
@Test @Test
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
fun `when pip is supported, the state value supportPip is true`() = runTest { fun `when pip is supported, the state value supportPip is true`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = true) val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(setPipParamsResult = { }),
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.supportPip).isTrue() assertThat(initialState.supportPip).isTrue()
} }
presenter.onDestroy()
} }
@Test @Test
@Config(sdk = [VERSION_CODES.S])
fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest { fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = true) val enterPipModeResult = lambdaRecorder<Boolean> { true }
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
enterPipModeResult = enterPipModeResult,
),
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isInPictureInPicture).isFalse() assertThat(initialState.isInPictureInPicture).isFalse()
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
presenter.onPictureInPictureModeChanged(true) enterPipModeResult.assertions().isCalledOnce()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true))
val pipState = awaitItem() val pipState = awaitItem()
assertThat(pipState.isInPictureInPicture).isTrue() assertThat(pipState.isInPictureInPicture).isTrue()
// User stops pip // User stops pip
presenter.onPictureInPictureModeChanged(false) initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.isInPictureInPicture).isFalse() assertThat(finalState.isInPictureInPicture).isFalse()
} }
presenter.onDestroy()
} }
@Test @Test
@Config(sdk = [VERSION_CODES.S]) fun `with webPipApi, when entering pip is supported, but web deny it, the call is finished`() = runTest {
fun `when onUserLeaveHint is called, the state value isInPictureInPicture becomes true`() = runTest { val handUpResult = lambdaRecorder<Unit> { }
val presenter = createPictureInPicturePresenter(supportPip = true) val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
handUpResult = handUpResult
),
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isInPictureInPicture).isFalse() initialState.eventSink(PictureInPictureEvents.SetPipController(FakePipController(canEnterPipResult = { false })))
presenter.onUserLeaveHint() initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
presenter.onPictureInPictureModeChanged(true) handUpResult.assertions().isCalledOnce()
}
}
@Test
fun `with webPipApi, when entering pip is supported, and web allows it, the state value isInPictureInPicture is true`() = runTest {
val enterPipModeResult = lambdaRecorder<Boolean> { true }
val enterPipResult = lambdaRecorder<Unit> { }
val exitPipResult = lambdaRecorder<Unit> { }
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
enterPipModeResult = enterPipModeResult
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(
PictureInPictureEvents.SetPipController(
FakePipController(
canEnterPipResult = { true },
enterPipResult = enterPipResult,
exitPipResult = exitPipResult,
)
)
)
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
enterPipModeResult.assertions().isCalledOnce()
enterPipResult.assertions().isNeverCalled()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true))
val pipState = awaitItem() val pipState = awaitItem()
assertThat(pipState.isInPictureInPicture).isTrue() assertThat(pipState.isInPictureInPicture).isTrue()
enterPipResult.assertions().isCalledOnce()
// User stops pip
exitPipResult.assertions().isNeverCalled()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false))
val finalState = awaitItem()
assertThat(finalState.isInPictureInPicture).isFalse()
exitPipResult.assertions().isCalledOnce()
} }
presenter.onDestroy()
} }
private fun createPictureInPicturePresenter( private fun createPictureInPicturePresenter(
supportPip: Boolean = true, supportPip: Boolean = true,
pipView: PipView? = FakePipView()
): PictureInPicturePresenter { ): PictureInPicturePresenter {
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
return PictureInPicturePresenter( return PictureInPicturePresenter(
pipSupportProvider = FakePipSupportProvider(supportPip), pipSupportProvider = FakePipSupportProvider(supportPip),
).apply { ).apply {
onCreate(activity.get()) setPipView(pipView)
} }
} }
} }

View file

@ -37,7 +37,7 @@ class CallScreenViewTest {
@Test @Test
fun `clicking on back when pip is not supported hangs up`() { fun `clicking on back when pip is not supported hangs up`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>() val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>(expectEvents = false) val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView( rule.setCallScreenView(
aCallScreenState( aCallScreenState(
eventSink = eventsRecorder eventSink = eventsRecorder
@ -51,6 +51,8 @@ class CallScreenViewTest {
eventsRecorder.assertSize(2) eventsRecorder.assertSize(2)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels } eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup } eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
pipEventsRecorder.assertSize(1)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
} }
@Test @Test
@ -69,7 +71,9 @@ class CallScreenViewTest {
rule.pressBack() rule.pressBack()
eventsRecorder.assertSize(1) eventsRecorder.assertSize(1)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels } eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
pipEventsRecorder.assertSingle(PictureInPictureEvents.EnterPictureInPicture) pipEventsRecorder.assertSize(2)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
pipEventsRecorder.assertTrue(1) { it == PictureInPictureEvents.EnterPictureInPicture }
} }
} }

View file

@ -36,6 +36,7 @@ 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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver 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.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
@ -43,11 +44,13 @@ import io.element.android.services.analytics.test.FakeScreenTracker
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
import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -86,8 +89,9 @@ class CallScreenPresenterTest {
@Test @Test
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest { fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda) val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val client = FakeMatrixClient().apply { val client = FakeMatrixClient(syncService = syncService).apply {
givenGetRoomResult(A_ROOM_ID, fakeRoom) givenGetRoomResult(A_ROOM_ID, fakeRoom)
} }
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
@ -216,7 +220,12 @@ class CallScreenPresenterTest {
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 = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient() val syncStateFlow = MutableStateFlow(SyncState.Idle)
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
this.startSyncLambda = startSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
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,
@ -230,7 +239,7 @@ class CallScreenPresenterTest {
}.test { }.test {
consumeItemsUntilTimeout() consumeItemsUntilTimeout()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running) assert(startSyncLambda).isCalledOnce()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -240,7 +249,12 @@ class CallScreenPresenterTest {
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 = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient() val syncStateFlow = MutableStateFlow(SyncState.Running)
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
this.stopSyncLambda = stopSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
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,
@ -262,7 +276,7 @@ class CallScreenPresenterTest {
job.cancelAndJoin() job.cancelAndJoin()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated) assert(stopSyncLambda).isCalledOnce()
} }
private fun TestScope.createCallScreenPresenter( private fun TestScope.createCallScreenPresenter(

View file

@ -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">"Nieuwe kamer"</string>
<string name="screen_create_room_add_people_title">"Mensen uitnodigen"</string>
<string name="screen_create_room_error_creating_room">"Er is een fout opgetreden bij het aanmaken van de kamer"</string>
<string name="screen_create_room_private_option_description">"Berichten in deze kamer zijn versleuteld. Versleuteling kan achteraf niet worden uitgeschakeld."</string>
<string name="screen_create_room_private_option_title">"Privé kamer (alleen op uitnodiging)"</string>
<string name="screen_create_room_public_option_description">"Berichten zijn niet versleuteld en iedereen kan ze lezen. Je kunt versleuteling later inschakelen."</string>
<string name="screen_create_room_public_option_title">"Openbare kamer (iedereen)"</string>
<string name="screen_create_room_room_name_label">"Naam van de kamer"</string>
<string name="screen_create_room_title">"Creëer een kamer"</string>
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
<string name="screen_start_chat_error_starting_chat">"Er is een fout opgetreden bij het starten van een chat"</string>
</resources>

View file

@ -2,11 +2,11 @@
<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_create_room_action_create_room">"Nowy pokój"</string> <string name="screen_create_room_action_create_room">"Nowy pokój"</string>
<string name="screen_create_room_add_people_title">"Zaproś znajomych"</string> <string name="screen_create_room_add_people_title">"Zaproś znajomych"</string>
<string name="screen_create_room_error_creating_room">"Wystąpił błąd podczas tworzenia pokoju"</string> <string name="screen_create_room_error_creating_room">"Wystąpił błąd w trakcie tworzenia pokoju"</string>
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string> <string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string> <string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string> <string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
<string name="screen_create_room_public_option_title">"Pokój publiczny (każdy)"</string> <string name="screen_create_room_public_option_title">"Pokój publiczny (wszyscy)"</string>
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string> <string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
<string name="screen_create_room_title">"Utwórz pokój"</string> <string name="screen_create_room_title">"Utwórz pokój"</string>
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string> <string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>

View file

@ -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">"Yangi xona"</string>
<string name="screen_create_room_add_people_title">"Odamlarni taklif qiling"</string>
<string name="screen_create_room_error_creating_room">"Xonani yaratishda xatolik yuz berdi"</string>
<string name="screen_create_room_private_option_description">"Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni ochirib bolmaydi."</string>
<string name="screen_create_room_private_option_title">"Shaxsiy xona (faqat taklif)"</string>
<string name="screen_create_room_public_option_description">"Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."</string>
<string name="screen_create_room_public_option_title">"Jamoat xonasi (har kim)"</string>
<string name="screen_create_room_room_name_label">"Xona nomi"</string>
<string name="screen_create_room_title">"Xonani yaratish"</string>
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>
<string name="screen_start_chat_error_starting_chat">"Suhbatni boshlashda xatolik yuz berdi"</string>
</resources>

View file

@ -5,7 +5,7 @@
<string name="screen_create_room_error_creating_room">"创建房间时出错"</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_description">"此聊天室中的消息已加密。加密无法禁用。"</string>
<string name="screen_create_room_private_option_title">"私人房间(仅限受邀者)"</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_description">"消息未加密,任何人都可以查看。可以稍后启用加密。"</string>
<string name="screen_create_room_public_option_title">"公共房间(任何人)"</string> <string name="screen_create_room_public_option_title">"公共房间(任何人)"</string>
<string name="screen_create_room_room_name_label">"房间名称"</string> <string name="screen_create_room_room_name_label">"房间名称"</string>
<string name="screen_create_room_title">"创建房间"</string> <string name="screen_create_room_title">"创建房间"</string>

View file

@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object EnterRecoveryKey : NavTarget data object EnterRecoveryKey : NavTarget
@Parcelize
data object ResetIdentity : NavTarget
} }
interface Callback : Plugin { interface Callback : Plugin {
@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
override fun onDone() { override fun onDone() {
plugins<Callback>().forEach { it.onDone() } plugins<Callback>().forEach { it.onDone() }
} }
override fun onResetKey() {
backstack.push(NavTarget.ResetIdentity)
}
}) })
.build() .build()
} }
@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
.callback(secureBackupEntryPointCallback) .callback(secureBackupEntryPointCallback)
.build() .build()
} }
is NavTarget.ResetIdentity -> {
secureBackupEntryPoint.nodeBuilder(this, buildContext)
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity))
.callback(object : SecureBackupEntryPoint.Callback {
override fun onDone() {
plugins<Callback>().forEach { it.onDone() }
}
})
.build()
}
} }
} }

View file

@ -98,7 +98,10 @@ class DefaultFtueService @Inject constructor(
} else { } else {
getNextStep(FtueStep.AnalyticsOptIn) getNextStep(FtueStep.AnalyticsOptIn)
} }
FtueStep.AnalyticsOptIn -> null FtueStep.AnalyticsOptIn -> {
updateState()
null
}
} }
private suspend fun isAnyStepIncomplete(): Boolean { private suspend fun isAnyStepIncomplete(): Boolean {

View 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">"Je kunt je instellingen later wijzigen."</string>
<string name="screen_notification_optin_title">"Sta meldingen toe en mis nooit meer een bericht"</string>
<string name="screen_welcome_bullet_1">"Oproepen, peilingen, zoekopdrachten en meer zullen later dit jaar worden toegevoegd."</string>
<string name="screen_welcome_bullet_2">"Berichtgeschiedenis voor versleutelde kamers is nog niet beschikbaar."</string>
<string name="screen_welcome_bullet_3">"We horen graag van je, laat ons weten wat je ervan vindt via de instellingenpagina."</string>
<string name="screen_welcome_button">"Aan de slag!"</string>
<string name="screen_welcome_subtitle">"Dit is wat je moet weten:"</string>
<string name="screen_welcome_title">"Welkom bij %1$s!"</string>
</resources>

View 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">"Sozlamalaringizni keyinroq o\'zgartirishingiz mumkin."</string>
<string name="screen_notification_optin_title">"Bildirishnomalarga ruxsat bering va hech qachon xabarni o\'tkazib yubormang"</string>
<string name="screen_welcome_bullet_1">"Qo\'ng\'iroqlar, so\'ro\'vlar, qidiruv va boshqalar shu yil oxirida qo\'shiladi."</string>
<string name="screen_welcome_bullet_2">"Shifrlangan xonalar uchun xabarlar tarixi hali mavjud emas."</string>
<string name="screen_welcome_bullet_3">"Biz sizdan eshitishni istardik, sozlamalar sahifasi orqali fikringizni bildiring."</string>
<string name="screen_welcome_button">"Qani ketdik!"</string>
<string name="screen_welcome_subtitle">"Buni bilishingiz kerak:"</string>
<string name="screen_welcome_title">"%1$sga Xush kelibsiz!"</string>
</resources>

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -107,7 +108,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
) = launch { ) = launch {
acceptedAction.runUpdatingState { acceptedAction.runUpdatingState {
joinRoom( joinRoom(
roomId = roomId, roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(), serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite, trigger = JoinedRoom.Trigger.Invite,
) )

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?"</string>
<string name="screen_invites_decline_chat_title">"Uitnodiging weigeren"</string>
<string name="screen_invites_decline_direct_chat_message">"Weet je zeker dat je deze privéchat met %1$s wilt weigeren?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chat weigeren"</string>
<string name="screen_invites_empty_list">"Geen uitnodigingen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) heeft je uitgenodigd"</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string> <string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string> <string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string> <string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string> <string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"</string>
<string name="screen_invites_decline_chat_title">"Taklifni rad etish"</string>
<string name="screen_invites_decline_direct_chat_message">"Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chatni rad etish"</string>
<string name="screen_invites_empty_list">"Takliflar yo\'q"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) sizni taklif qildi"</string>
</resources>

View file

@ -23,7 +23,9 @@ import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
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.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -178,8 +180,8 @@ class AcceptDeclineInvitePresenterTest {
@Test @Test
fun `present - accepting invite error flow`() = runTest { fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomId: RoomId, _: List<String>, _: JoinedRoom.Trigger -> val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomId")) Result.failure<Unit>(RuntimeException("Failed to join room $roomIdOrAlias"))
} }
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure) val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure)
presenter.test { presenter.test {
@ -208,7 +210,7 @@ class AcceptDeclineInvitePresenterTest {
assert(joinRoomFailure) assert(joinRoomFailure)
.isCalledOnce() .isCalledOnce()
.with( .with(
value(A_ROOM_ID), value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()), value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite) value(JoinedRoom.Trigger.Invite)
) )
@ -222,7 +224,7 @@ class AcceptDeclineInvitePresenterTest {
val fakeNotificationCleaner = FakeNotificationCleaner( val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
) )
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger -> val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit) Result.success(Unit)
} }
val presenter = createAcceptDeclineInvitePresenter( val presenter = createAcceptDeclineInvitePresenter(
@ -248,7 +250,7 @@ class AcceptDeclineInvitePresenterTest {
assert(joinRoomSuccess) assert(joinRoomSuccess)
.isCalledOnce() .isCalledOnce()
.with( .with(
value(A_ROOM_ID), value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()), value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite) value(JoinedRoom.Trigger.Invite)
) )
@ -271,7 +273,7 @@ class AcceptDeclineInvitePresenterTest {
private fun createAcceptDeclineInvitePresenter( private fun createAcceptDeclineInvitePresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ -> joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit) Result.success(Unit)
}, },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),

View file

@ -96,7 +96,7 @@ class JoinRoomPresenter @AssistedInject constructor(
} }
else -> { else -> {
value = ContentState.Loading(roomIdOrAlias) value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreviewFromRoomId(roomId, serverNames) val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
value = result.fold( value = result.fold(
onSuccess = { roomPreview -> onSuccess = { roomPreview ->
roomPreview.toContentState() roomPreview.toContentState()
@ -153,7 +153,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private fun CoroutineScope.joinRoom(joinAction: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.joinRoom(joinAction: MutableState<AsyncAction<Unit>>) = launch {
joinAction.runUpdatingState { joinAction.runUpdatingState {
joinRoom.invoke( joinRoom.invoke(
roomId = roomId, roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames, serverNames = serverNames,
trigger = trigger trigger = trigger
) )

View file

@ -37,7 +37,11 @@ data class JoinRoomState(
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
val joinAuthorisationStatus = when (contentState) { val joinAuthorisationStatus = when (contentState) {
// Use the join authorisation status from the loaded content state
is ContentState.Loaded -> contentState.joinAuthorisationStatus is ContentState.Loaded -> contentState.joinAuthorisationStatus
// Assume that if the room is unknown, the user can join it
is ContentState.UnknownRoom -> JoinAuthorisationStatus.CanJoin
// Otherwise assume that the user can't join the room
else -> JoinAuthorisationStatus.Unknown else -> JoinAuthorisationStatus.Unknown
} }
} }

View 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_join_room_join_action">"Dołącz do pokoju"</string>
<string name="screen_join_room_knock_action">"Zapukaj, by dołączyć"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web."</string>
<string name="screen_join_room_space_not_supported_title">"Przestrzenie nie są jeszcze obsługiwane"</string>
<string name="screen_join_room_subtitle_knock">"Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy."</string>
<string name="screen_join_room_subtitle_no_preview">"Musisz być członkiem tego pokoju, aby wyświetlić historię wiadomości."</string>
<string name="screen_join_room_title_knock">"Chcesz dołączyć do tego pokoju?"</string>
<string name="screen_join_room_title_no_preview">"Podgląd nie jest dostępny"</string>
</resources>

View 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_join_room_join_action">"Gå med i rummet"</string>
<string name="screen_join_room_knock_action">"Knacka för att gå med"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s stöder inte utrymmen än. Du kan komma åt utrymmen på webben."</string>
<string name="screen_join_room_space_not_supported_title">"Utrymmen stöds inte ännu"</string>
<string name="screen_join_room_subtitle_knock">"Klicka på knappen nedan så kommer en rumsadministratör att meddelas. Du kommer att kunna gå med i konversationen när den har godkänts."</string>
<string name="screen_join_room_subtitle_no_preview">"Du måste vara medlem i det här rummet för att se meddelandehistoriken."</string>
<string name="screen_join_room_title_knock">"Vill du gå med i det här rummet?"</string>
<string name="screen_join_room_title_no_preview">"Förhandsgranskning är inte tillgänglig"</string>
</resources>

View 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_join_room_join_action">"Приєднатися до кімнати"</string>
<string name="screen_join_room_knock_action">"Постукати, щоб приєднатися"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s ще не підтримує простори. Ви можете отримати доступ до них в вебверсії."</string>
<string name="screen_join_room_space_not_supported_title">"Простори поки що не підтримуються"</string>
<string name="screen_join_room_subtitle_knock">"Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення."</string>
<string name="screen_join_room_subtitle_no_preview">"Ви мусите бути учасником цієї кімнати, щоб переглядати історію повідомлень."</string>
<string name="screen_join_room_title_knock">"Хочете приєднатися до цієї кімнати?"</string>
<string name="screen_join_room_title_no_preview">"Попередній перегляд недоступний"</string>
</resources>

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias 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.UserId 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.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -180,7 +181,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is joined with success, all the parameters are provided`() = runTest { fun `present - when room is joined with success, all the parameters are provided`() = runTest {
val aTrigger = JoinedRoom.Trigger.MobilePermalink val aTrigger = JoinedRoom.Trigger.MobilePermalink
val joinRoomLambda = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger -> val joinRoomLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit) Result.success(Unit)
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -201,7 +202,7 @@ class JoinRoomPresenterTest {
} }
joinRoomLambda.assertions() joinRoomLambda.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_ROOM_ID), value(A_SERVER_LIST), value(aTrigger)) .with(value(A_ROOM_ID.toRoomIdOrAlias()), value(A_SERVER_LIST), value(aTrigger))
} }
} }
@ -366,7 +367,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded`() = runTest { fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.success( Result.success(
RoomPreview( RoomPreview(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -411,7 +412,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest { fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} }
) )
@ -449,7 +450,7 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest { fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.failure(Exception("403")) Result.failure(Exception("403"))
} }
) )
@ -474,7 +475,7 @@ class JoinRoomPresenterTest {
serverNames: List<String> = emptyList(), serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite, trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ -> joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit) Result.success(Unit)
}, },
knockRoom: KnockRoom = FakeKnockRoom(), knockRoom: KnockRoom = FakeKnockRoom(),

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Weet je zeker dat je dit gesprek wilt verlaten? Dit gesprek is niet openbaar en je kunt niet opnieuw deelnemen zonder een uitnodiging."</string>
<string name="leave_room_alert_empty_subtitle">"Weet je zeker dat je deze kamer wilt verlaten? Je bent de enige persoon hier. Als je weggaat, kan er in de toekomst niemand meer toetreden, ook jij niet."</string>
<string name="leave_room_alert_private_subtitle">"Weet je zeker dat je deze kamer wilt verlaten? Deze kamer is niet openbaar en je kunt niet opnieuw deelnemen zonder een uitnodiging."</string>
<string name="leave_room_alert_subtitle">"Weet je zeker dat je de kamer wilt verlaten?"</string>
</resources>

View 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="leave_room_alert_empty_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Siz bu yerda yagona odamsiz. Agar siz tark etsangiz, kelajakda hech kim qo\'shila olmaydi, jumladan siz ham."</string>
<string name="leave_room_alert_private_subtitle">"Bu xonani tark etmoqchi ekanligingizga ishonchingiz komilmi? Bu xona ochiq emas va siz taklifsiz qayta qoshila olmaysiz."</string>
<string name="leave_room_alert_subtitle">"Xonani tark etmoqchi ekanligingizga ishonchingiz komilmi?"</string>
</resources>

View file

@ -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="leave_conversation_alert_subtitle">"您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。"</string> <string name="leave_conversation_alert_subtitle">"您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。"</string>
<string name="leave_room_alert_empty_subtitle">"确定要离开这个房间吗?这里只有你一个人,如果你走了,包括你在内的所有人都无法进入此房间。"</string> <string name="leave_room_alert_empty_subtitle">"确定要离开这个房间吗?这里只有你一个人。如果你离开此房间,包括你在内的所有人都将无法进入。"</string>
<string name="leave_room_alert_private_subtitle">"你确定要离开这个房间吗?这个房间不是公开的,如果没有邀请,你将无法重新加入。"</string> <string name="leave_room_alert_private_subtitle">"确定要离开这个房间吗?此房间不公开,没有邀请你将无法重新加入。"</string>
<string name="leave_room_alert_subtitle">"确定要离开房间吗?"</string> <string name="leave_room_alert_subtitle">"确定要离开房间吗?"</string>
</resources> </resources>

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.cryptography.api) implementation(projects.libraries.cryptography.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.features.logout.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api) implementation(projects.services.appnavstate.api)
@ -59,4 +60,5 @@ dependencies {
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.features.logout.test)
} }

View file

@ -29,7 +29,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -41,7 +41,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor( class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager, private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager, private val biometricUnlockManager: BiometricUnlockManager,
private val signOut: SignOut, private val logoutUseCase: LogoutUseCase,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper, private val pinUnlockHelper: PinUnlockHelper,
) : Presenter<PinUnlockState> { ) : Presenter<PinUnlockState> {
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch { private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend { suspend {
signOut() logoutUseCase.logout(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction) }.runCatchingUpdatingState(signOutAction)
} }
} }

View file

@ -18,9 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SessionScope
@ContributesTo(AppScope::class) @ContributesTo(SessionScope::class)
interface PinUnlockBindings { interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity) fun inject(activity: PinUnlockActivity)
} }

View file

@ -1,40 +0,0 @@
/*
* 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.features.lockscreen.impl.unlock.signout
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSignOut @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val matrixClientProvider: MatrixClientProvider,
) : SignOut {
override suspend fun invoke(): String? {
val currentSession = authenticationService.getLatestSessionId()
return if (currentSession != null) {
matrixClientProvider.getOrRestore(currentSession)
.getOrThrow()
.logout(ignoreSdkError = true)
} else {
error("No session to sign out")
}
}
}

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrische authenticatie"</string>
<string name="screen_app_lock_biometric_unlock">"biometrische ontgrendeling"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Ontgrendelen met biometrie"</string>
<string name="screen_app_lock_forgot_pin">"Pincode vergeten?"</string>
<string name="screen_app_lock_settings_change_pin">"Pincode wijzigen"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrische ontgrendeling toestaan"</string>
<string name="screen_app_lock_settings_remove_pin">"Pincode verwijderen"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Weet je zeker dat je de pincode wilt verwijderen?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Pincode verwijderen?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s toestaan"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ik gebruik liever een pincode"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Bespaar jezelf tijd en gebruik %1$s om de app elke keer te ontgrendelen"</string>
<string name="screen_app_lock_setup_choose_pin">"Kies je pincode"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bevestig pincode"</string>
<string name="screen_app_lock_setup_pin_context">"Vergrendel %1$s om je chats extra te beveiligen.
Kies iets dat je kunt onthouden. Als je deze pincode vergeet, word je uitgelogd bij de app."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Vanwege veiligheidsredenen kun je dit niet als je pincode kiezen"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Kies een andere pincode"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Voer dezelfde pincode twee keer in"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Pincodes komen niet overeen"</string>
<string name="screen_app_lock_signout_alert_message">"Je moet opnieuw inloggen en een nieuwe pincode aanmaken om verder te gaan"</string>
<string name="screen_app_lock_signout_alert_title">"Je wordt uitgelogd"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Je hebt %1$d poging om te ontgrendelen"</item>
<item quantity="other">"Je hebt %1$d pogingen om te ontgrendelen"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Verkeerde pincode. Je hebt nog %1$d kans"</item>
<item quantity="other">"Verkeerde pincode. Je hebt nog %1$d kansen"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrie gebruiken"</string>
<string name="screen_app_lock_use_pin_android">"Pincode gebruiken"</string>
<string name="screen_signout_in_progress_dialog_content">"Uitloggen…"</string>
</resources>

View file

@ -16,11 +16,11 @@
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string> <string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów. <string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz tego PINU, zostaniesz wylogowany z aplikacji."</string> Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz ten PIN, zostaniesz wylogowany z aplikacji."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PINU ze względów bezpieczeństwa"</string> <string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PIN\'u ze względów bezpieczeństwa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string> <string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINY nie pasują do siebie"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN\'y nie pasują do siebie"</string>
<string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string> <string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string>
<string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string> <string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string>
<plurals name="screen_app_lock_subtitle"> <plurals name="screen_app_lock_subtitle">

View file

@ -1,14 +1,27 @@
<?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_app_lock_biometric_authentication">"autenticação por biometria"</string>
<string name="screen_app_lock_biometric_unlock">"desbloqueio por biometria"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string> <string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Mudar código de PIN"</string> <string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string> <string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string> <string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string> <string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string> <string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Permitir %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Prefiro usar o PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Poupe tempo e use %1$s para desbloquear o aplicativo todas as vezes"</string>
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string> <string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string> <string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Bloqueie o %1$s para adicionar uma segurança extra às suas conversas.
Escolha algo memorável. Se você esquecer este PIN, você será desconectado do app."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Você não pode escolher este PIN por razões de segurança"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Escolha um PIN diferente"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Por favor, insira o mesmo PIN duas vezes"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string>
<string name="screen_app_lock_signout_alert_message">"Você terá que fazer login novamente e criar um novo PIN para prosseguir"</string>
<string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string> <string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string>
<plurals name="screen_app_lock_subtitle"> <plurals name="screen_app_lock_subtitle">
<item quantity="one">"Você tem %1$d tentativa de debloqueio"</item> <item quantity="one">"Você tem %1$d tentativa de debloqueio"</item>
@ -18,5 +31,7 @@
<item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item> <item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item>
<item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item> <item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item>
</plurals> </plurals>
<string name="screen_app_lock_use_biometric_android">"Usar biometria"</string>
<string name="screen_app_lock_use_pin_android">"Usar PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string> <string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
</resources> </resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_in_progress_dialog_content">"Chiqish…"</string>
</resources>

View file

@ -1,61 +0,0 @@
/*
* 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.features.lockscreen.impl.unlock
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultSignOutTest {
private val matrixClient = FakeMatrixClient()
private val authenticationService = FakeMatrixAuthenticationService()
private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
private val sut = DefaultSignOut(authenticationService, matrixClientProvider)
@Test
fun `when no active session then it throws`() = runTest {
authenticationService.getLatestSessionIdLambda = { null }
val result = runCatching { sut.invoke() }
assertThat(result.isFailure).isTrue()
}
@Test
fun `with one active session and successful logout on client`() = runTest {
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> null }
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
matrixClient.logoutLambda = logoutLambda
val result = runCatching { sut.invoke() }
assertThat(result.isSuccess).isTrue()
assert(logoutLambda).isCalledOnce()
}
@Test
fun `with one active session and and failed logout on client`() = runTest {
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> error("Failed to logout") }
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
matrixClient.logoutLambda = logoutLambda
val result = runCatching { sut.invoke() }
assertThat(result.isFailure).isTrue()
assert(logoutLambda).isCalledOnce()
}
}

View file

@ -28,7 +28,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -106,9 +106,9 @@ class PinUnlockPresenterTest {
@Test @Test
fun `present - forgot pin flow`() = runTest { fun `present - forgot pin flow`() = runTest {
val signOutLambda = lambdaRecorder<String?> { null } val signOutLambda = lambdaRecorder<Boolean, String> { "" }
val signOut = FakeSignOut(signOutLambda) val signOut = FakeLogoutUseCase(signOutLambda)
val presenter = createPinUnlockPresenter(this, signOut = signOut) val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -135,7 +135,7 @@ class PinUnlockPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
} }
assert(signOutLambda).isCalledOnce().withNoParameter() assert(signOutLambda).isCalledOnce()
} }
} }
@ -147,7 +147,7 @@ class PinUnlockPresenterTest {
scope: CoroutineScope, scope: CoroutineScope,
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(), biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
signOut: SignOut = FakeSignOut(), logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
): PinUnlockPresenter { ): PinUnlockPresenter {
val pinCodeManager = aPinCodeManager().apply { val pinCodeManager = aPinCodeManager().apply {
addCallback(callback) addCallback(callback)
@ -156,7 +156,7 @@ class PinUnlockPresenterTest {
return PinUnlockPresenter( return PinUnlockPresenter(
pinCodeManager = pinCodeManager, pinCodeManager = pinCodeManager,
biometricUnlockManager = biometricUnlockManager, biometricUnlockManager = biometricUnlockManager,
signOut = signOut, logoutUseCase = logoutUseCase,
coroutineScope = scope, coroutineScope = scope,
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
) )

View file

@ -50,6 +50,7 @@ dependencies {
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.qrcode) implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
implementation(platform(libs.network.retrofit.bom)) implementation(platform(libs.network.retrofit.bom))
implementation(libs.network.retrofit) implementation(libs.network.retrofit)
@ -65,6 +66,7 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

View file

@ -36,12 +36,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.api.LoginFlowType import io.element.android.features.login.api.LoginFlowType
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcActionFlow
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
import io.element.android.features.login.impl.oidc.webview.OidcNode
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
@ -56,6 +51,9 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.oidc.api.OidcEntryPoint
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -64,11 +62,10 @@ import kotlinx.parcelize.Parcelize
class LoginFlowNode @AssistedInject constructor( class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
private val customTabHandler: CustomTabHandler,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val defaultLoginUserStory: DefaultLoginUserStory, private val defaultLoginUserStory: DefaultLoginUserStory,
private val oidcActionFlow: OidcActionFlow, private val oidcActionFlow: OidcActionFlow,
private val oidcEntryPoint: OidcEntryPoint,
) : BaseFlowNode<LoginFlowNode.NavTarget>( ) : BaseFlowNode<LoginFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@ -146,11 +143,11 @@ class LoginFlowNode @AssistedInject constructor(
) )
val callback = object : ConfirmAccountProviderNode.Callback { val callback = object : ConfirmAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) { override fun onOidcDetails(oidcDetails: OidcDetails) {
if (customTabAvailabilityChecker.supportCustomTab()) { if (oidcEntryPoint.canUseCustomTab()) {
// In this case open a Chrome Custom tab // In this case open a Chrome Custom tab
activity?.let { activity?.let {
customChromeTabStarted = true customChromeTabStarted = true
customTabHandler.open(it, darkTheme, oidcDetails.url) oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
} }
} else { } else {
// Fallback to WebView mode // Fallback to WebView mode
@ -201,8 +198,7 @@ class LoginFlowNode @AssistedInject constructor(
createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback)) createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback))
} }
is NavTarget.OidcView -> { is NavTarget.OidcView -> {
val input = OidcNode.Inputs(navTarget.oidcDetails) oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
createNode<OidcNode>(buildContext, plugins = listOf(input))
} }
is NavTarget.WaitList -> { is NavTarget.WaitList -> {
val inputs = WaitListNode.Inputs( val inputs = WaitListNode.Inputs(

View file

@ -27,15 +27,15 @@ import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: Params, @Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val defaultOidcActionFlow: DefaultOidcActionFlow, private val oidcActionFlow: OidcActionFlow,
private val defaultLoginUserStory: DefaultLoginUserStory, private val defaultLoginUserStory: DefaultLoginUserStory,
) : Presenter<ConfirmAccountProviderState> { ) : Presenter<ConfirmAccountProviderState> {
data class Params( data class Params(
@ -65,7 +65,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
defaultOidcActionFlow.collect { oidcAction -> oidcActionFlow.collect { oidcAction ->
if (oidcAction != null) { if (oidcAction != null) {
onOidcAction(oidcAction, loginFlowAction) onOidcAction(oidcAction, loginFlowAction)
} }
@ -133,6 +133,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
} }
} }
} }
defaultOidcActionFlow.reset() oidcActionFlow.reset()
} }
} }

View file

@ -39,6 +39,20 @@
<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_device_not_signed_in_scan_state_description">"Melde dich auf deinem anderen Gerät an und versuche es dann noch einmal oder verwende ein anderes Gerät, das bereits angemeldet ist."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Anderes Gerät ist nicht angemeldet"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Die Anmeldung wurde auf dem anderen Gerät abgebrochen."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Anmeldeanfrage abgebrochen"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Die Anmeldung auf dem anderen Gerät wurde abgelehnt."</string>
<string name="screen_qr_code_login_error_declined_title">"Anmelden abgelehnt"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Die Anmeldung ist abgelaufen. Bitte versuchen Sie es erneut."</string>
<string name="screen_qr_code_login_error_expired_title">"Die Anmeldung wurde nicht rechtzeitig abgeschlossen"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Dein anderes Gerät unterstützt die Anmeldung bei %s mit einem QR-Code nicht.
Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Gerät."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-Code wird nicht unterstützt"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ihr Kontoanbieter unterstützt %1$s nicht."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$swird nicht unterstützt"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</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>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Wijzig accountprovider"</string>
<string name="screen_account_provider_form_hint">"Homeserver-adres"</string>
<string name="screen_account_provider_form_notice">"Voer een zoekterm of een domeinnaam in."</string>
<string name="screen_account_provider_form_subtitle">"Zoek naar een bedrijf, community of privéserver."</string>
<string name="screen_account_provider_form_title">"Vind een accountprovider"</string>
<string name="screen_account_provider_signin_subtitle">"Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."</string>
<string name="screen_account_provider_signin_title">"Je staat op het punt om je aan te melden bij %s"</string>
<string name="screen_account_provider_signup_subtitle">"Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."</string>
<string name="screen_account_provider_signup_title">"Je staat op het punt een account aan te maken op %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org is een grote, gratis server op het openbare Matrix-netwerk voor veilige, gedecentraliseerde communicatie, beheerd door de Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Anders"</string>
<string name="screen_change_account_provider_subtitle">"Gebruik een andere accountprovider, zoals je eigen privéserver of een zakelijke account."</string>
<string name="screen_change_account_provider_title">"Wijzig accountprovider"</string>
<string name="screen_change_server_error_invalid_homeserver">"We konden deze homeserver niet bereiken. Controleer of je de homeserver-URL juist hebt ingevoerd. Als de URL juist is, neem dan contact op met de beheerder van je homeserver voor verdere hulp."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Deze server ondersteunt op dit moment geen sliding sync."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Je kunt alleen verbinding maken met een bestaande server die sliding sync ondersteunt. De beheerder van de homeserver moet dit configureren. %1$s"</string>
<string name="screen_change_server_subtitle">"Wat is het adres van je server?"</string>
<string name="screen_change_server_title">"Selecteer je server"</string>
<string name="screen_login_error_deactivated_account">"Dit account is gedeactiveerd."</string>
<string name="screen_login_error_invalid_credentials">"Onjuiste gebruikersnaam en/of wachtwoord"</string>
<string name="screen_login_error_invalid_user_id">"Dit is geen geldige gebruikers-ID. Verwacht formaat: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Deze server is geconfigureerd om verversingstokens te gebruiken. Deze worden niet ondersteund bij inloggen met een wachtwoord."</string>
<string name="screen_login_error_unsupported_authentication">"De geselecteerde homeserver ondersteunt geen wachtwoord of OIDC aanmelding. Neem contact op met je beheerder of kies een andere homeserver."</string>
<string name="screen_login_form_header">"Vul je gegevens in"</string>
<string name="screen_login_subtitle">"Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."</string>
<string name="screen_login_title">"Welkom terug!"</string>
<string name="screen_login_title_with_homeserver">"Inloggen bij %1$s"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Probeer het opnieuw"</string>
<string name="screen_server_confirmation_change_server">"Accountprovider wijzigen"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Een privéserver voor medewerkers van Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."</string>
<string name="screen_server_confirmation_message_register">"Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."</string>
<string name="screen_server_confirmation_title_login">"Je staat op het punt je aan te melden bij %1$s"</string>
<string name="screen_server_confirmation_title_register">"Je staat op het punt een account aan te maken op %1$s"</string>
<string name="screen_waitlist_message">"Er is momenteel veel vraag naar %1$s op %2$s. Kom over een paar dagen terug naar de app en probeer het opnieuw.
Bedankt voor je geduld!"</string>
<string name="screen_waitlist_message_success">"Welkom bij %1$s!"</string>
<string name="screen_waitlist_title">"Je bent er bijna."</string>
<string name="screen_waitlist_title_success">"Je bent binnen."</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="screen_account_provider_form_subtitle">"Szukaj serwera firmowego, społeczności lub prywatnego."</string> <string name="screen_account_provider_form_subtitle">"Szukaj serwera firmowego, społeczności lub prywatnego."</string>
<string name="screen_account_provider_form_title">"Znajdź dostawcę konta"</string> <string name="screen_account_provider_form_title">"Znajdź dostawcę konta"</string>
<string name="screen_account_provider_signin_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string> <string name="screen_account_provider_signin_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
<string name="screen_account_provider_signin_title">"Zamierzasz się zalogować %s"</string> <string name="screen_account_provider_signin_title">"Zamierzasz zalogować się do %s"</string>
<string name="screen_account_provider_signup_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string> <string name="screen_account_provider_signup_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
<string name="screen_account_provider_signup_title">"Zamierzasz założyć konto na %s"</string> <string name="screen_account_provider_signup_title">"Zamierzasz założyć konto na %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org jest ogromnym i darmowym serwerem na publicznej sieci Matrix zapewniający bezpieczną i zdecentralizowaną komunikację zarządzaną przez Fundację Matrix.org."</string> <string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org jest ogromnym i darmowym serwerem na publicznej sieci Matrix zapewniający bezpieczną i zdecentralizowaną komunikację zarządzaną przez Fundację Matrix.org."</string>
@ -14,20 +14,64 @@
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string> <string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string> <string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string> <string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync nie jest dostępny z powodu problemu w znanym pliku:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Ten serwer obecnie nie obsługuje technologii Sliding Sync."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Ten serwer obecnie nie obsługuje technologii Sliding Sync."</string>
<string name="screen_change_server_form_header">"Adres URL serwera domowego"</string> <string name="screen_change_server_form_header">"URL serwera domowego"</string>
<string name="screen_change_server_form_notice">"Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s"</string> <string name="screen_change_server_form_notice">"Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s"</string>
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string> <string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>
<string name="screen_change_server_title">"Wybierz swój serwer"</string> <string name="screen_change_server_title">"Wybierz swój serwer"</string>
<string name="screen_login_error_deactivated_account">"To konto zostało dezaktywowane."</string> <string name="screen_login_error_deactivated_account">"To konto zostało dezaktywowane."</string>
<string name="screen_login_error_invalid_credentials">"Nieprawidłowa nazwa użytkownika i/lub hasło"</string> <string name="screen_login_error_invalid_credentials">"Nieprawidłowa nazwa użytkownika i/lub hasło"</string>
<string name="screen_login_error_invalid_user_id">"To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'"</string> <string name="screen_login_error_invalid_user_id">"To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Ten serwer został skonfigurowany do korzystania z tokenów odświeżania. Nie są one obsługiwane, gdy korzystasz z hasła."</string>
<string name="screen_login_error_unsupported_authentication">"Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy."</string> <string name="screen_login_error_unsupported_authentication">"Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy."</string>
<string name="screen_login_form_header">"Wprowadź swoje dane"</string> <string name="screen_login_form_header">"Wprowadź swoje dane"</string>
<string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string> <string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
<string name="screen_login_title">"Witaj ponownie!"</string> <string name="screen_login_title">"Witaj ponownie!"</string>
<string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string> <string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Nawiązanie bezpiecznego połączenia"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nie udało się nawiązać bezpiecznego połączenia z nowym urządzeniem. Twoje istniejące urządzenia są nadal bezpieczne i nie musisz się o nie martwić."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teraz?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Spróbuj zalogować się ponownie za pomocą kodu QR, jeśli byłby to problem z siecią"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jeśli napotkasz ten sam problem, użyj innej sieci Wi-FI lub danych mobilnych"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jeśli to nie zadziała, zaloguj się ręcznie"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Połączenie nie jest bezpieczne"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Zostaniesz poproszony o wprowadzenie dwóch cyfr widocznych na tym urządzeniu."</string>
<string name="screen_qr_code_login_device_code_title">"Wprowadź numer poniżej na innym urządzeniu"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Zaloguj się na drugie urządzenie lub użyj tego, które jest już zalogowane, a następnie spróbuj ponownie."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Drugie urządzenie nie jest zalogowane"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Logowanie zostało anulowane na drugim urządzeniu."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Prośba o logowanie została anulowana"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Logowanie zostało odrzucone na drugim urządzeniu."</string>
<string name="screen_qr_code_login_error_declined_title">"Logowanie odrzucone"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Logowanie wygasło. Spróbuj ponownie."</string>
<string name="screen_qr_code_login_error_expired_title">"Logowanie nie zostało ukończone na czas"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Twoje drugie urządzenie nie wspiera logowania się do %s za pomocą kodu QR.
Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"Kod QR nie jest wspierany"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Twój dostawca konta nie obsługuje %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie jest wspierany"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Gotowy do skanowania"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Otwórz %1$s na urządzeniu stacjonarnym"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Kliknij na swój awatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Wybierz %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Powiąż nowe urządzenie”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Zeskanuj kod QR za pomocą tego urządzenia"</string>
<string name="screen_qr_code_login_initial_state_title">"Otwórz %1$s na innym urządzeniu, aby uzyskać kod QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Użyj kodu QR widocznego na drugim urządzeniu."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Spróbuj ponownie"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Spróbuj ponownie"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Błędny kod QR"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Przejdź do ustawień aparatu"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Musisz przyznać uprawnienia %1$s do korzystania z kamery, aby kontynuować."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Zezwól na dostęp do kamery, aby zeskanować kod QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Skanuj kod QR"</string>
<string name="screen_qr_code_login_start_over_button">"Zacznij od nowa"</string>
<string name="screen_qr_code_login_unknown_error_description">"Wystąpił nieoczekiwany błąd. Spróbuj ponownie."</string>
<string name="screen_qr_code_login_verify_code_loading">"Oczekiwanie na drugie urządzenie"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Twój dostawca konta może poprosić o podany kod, aby zweryfikować logowanie."</string>
<string name="screen_qr_code_login_verify_code_title">"Twój kod weryfikacyjny"</string>
<string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string> <string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>

View file

@ -30,7 +30,48 @@
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string> <string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
<string name="screen_login_title">"Välkommen tillbaka!"</string> <string name="screen_login_title">"Välkommen tillbaka!"</string>
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string> <string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Upprättar en säker anslutning"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"En säker anslutning kunde inte göras till den nya enheten. Dina befintliga enheter är fortfarande säkra och du behöver inte oroa dig för dem."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Nu då?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Pröva att logga in igen med en QR-kod ifall detta skulle vara ett nätverksproblem"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Om du stöter på samma problem, prova ett annat wifi-nätverk eller använd din mobildata istället för wifi"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Om det inte fungerar, logga in manuellt"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Anslutningen är inte säker"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Du kommer att bli ombedd att ange de två siffrorna som visas på den här enheten."</string>
<string name="screen_qr_code_login_device_code_title">"Ange numret nedan på din andra enhet"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Logga in på din andra enhet och försök sedan igen, eller använd en annan enhet som redan är inloggad."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Den andra enheten är inte inloggad"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Inloggningen avbröts på den andra enheten."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Inloggningsförfrågan avbröts"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Inloggningen avvisades på den andra enheten."</string>
<string name="screen_qr_code_login_error_declined_title">"Inloggning avvisad"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Inloggningen har löpt ut. Vänligen försök igen."</string>
<string name="screen_qr_code_login_error_expired_title">"Inloggningen slutfördes inte i tid"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Din andra enhet stöder inte inloggning i %s med en QR-kod.
Prova att logga in manuellt eller skanna QR-koden med en annan enhet."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-kod stöds inte"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Din kontoleverantör stöder inte %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s stöds inte"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Redo att skanna"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Öppna %1$s på en skrivbordsenhet"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klicka på din avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Välj %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"”Länka ny enhet”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Skanna QR-koden med den här enheten"</string>
<string name="screen_qr_code_login_initial_state_title">"Öppna %1$s på en annan enhet för att få QR-koden"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Använd QR-koden som visas på den andra enheten."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Fel QR-kod"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Gå till kamerainställningar"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du måste ge tillstånd för %1$s att använda enhetens kamera för att kunna fortsätta."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Tillåt kameraåtkomst för att skanna QR-koden"</string>
<string name="screen_qr_code_login_scanning_state_title">"Skanna QR-koden"</string>
<string name="screen_qr_code_login_start_over_button">"Börja om"</string>
<string name="screen_qr_code_login_unknown_error_description">"Ett oväntat fel inträffade. Vänligen försök igen."</string>
<string name="screen_qr_code_login_verify_code_loading">"Väntar på din andra enhet"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Din kontoleverantör kan be om följande kod för att verifiera inloggningen."</string>
<string name="screen_qr_code_login_verify_code_title">"Din verifieringskod"</string>
<string name="screen_server_confirmation_change_server">"Byt kontoleverantör"</string> <string name="screen_server_confirmation_change_server">"Byt kontoleverantör"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"En privat server för Element-anställda."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"En privat server för Element-anställda."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>

View file

@ -30,7 +30,48 @@
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string> <string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
<string name="screen_login_title">"З поверненням!"</string> <string name="screen_login_title">"З поверненням!"</string>
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string> <string name="screen_login_title_with_homeserver">"Увійти в %1$s"</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">"Спробуйте увійти ще раз за допомогою QR-коду, якщо це була проблема з мережею"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість 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_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_device_not_signed_in_scan_state_description">"Увійдіть на іншому пристрої та спробуйте ще раз або скористайтеся іншим пристроєм, що вже в обліковому записі."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Інший пристрій не ввійшов"</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_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">"Відскануйте 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_retry_button">"Спробуйте ще раз"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неправильний QR-код"</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">"Відскануйте QR-код"</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_server_confirmation_change_server">"Змінити провайдера облікового запису"</string> <string name="screen_server_confirmation_change_server">"Змінити провайдера облікового запису"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Hisob provayderini o\'zgartiring"</string>
<string name="screen_account_provider_form_hint">"Uy server manzili"</string>
<string name="screen_account_provider_form_notice">"Qidiruv so\'zini yoki domen manzilini kiriting."</string>
<string name="screen_account_provider_form_subtitle">"Kompaniya, jamoa yoki shaxsiy serverni qidiring."</string>
<string name="screen_account_provider_form_title">"Hisob provayderini toping"</string>
<string name="screen_account_provider_signin_subtitle">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
<string name="screen_account_provider_signin_title">"Siz %sga kirmoqchisiz"</string>
<string name="screen_account_provider_signup_subtitle">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
<string name="screen_account_provider_signup_title">"Siz %sda hisob yaratmoqchisiz"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org - bu Matrix.org Jamg\'armasi tomonidan boshqariladigan xavfsiz, markazlashtirilmagan aloqa uchun ommaviy Matrix tarmog\'idagi katta, bepul server."</string>
<string name="screen_change_account_provider_other">"Boshqa"</string>
<string name="screen_change_account_provider_subtitle">"Shaxsiy serveringiz yoki ishchi hisob qaydnomangiz kabi boshqa hisob provayderidan foydalaning."</string>
<string name="screen_change_account_provider_title">"Hisob provayderini o\'zgartiring"</string>
<string name="screen_change_server_error_invalid_homeserver">"Bu uy serveriga kira olmadik. Iltimos, uy serverining URL manzilini to\'ri kiritganingizni tekshiring. Agar URL toʻgʻri boʻlsa, qoʻshimcha yordam olish uchun uy serveri administratoriga murojaat qiling."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Hozirda bu server siljish sinxronlashni qollab-quvvatlamaydi."</string>
<string name="screen_change_server_form_header">"Uy serverining URL manzili"</string>
<string name="screen_change_server_form_notice">"Siz faqat siljish sinxronlashni qo\'llab-quvvatlaydigan mavjud serverga ulanishingiz mumkin. Uy serveringiz administratori uni sozlashi kerak.%1$s"</string>
<string name="screen_change_server_subtitle">"Serveringizning manzili nima?"</string>
<string name="screen_change_server_title">"Serveringizni tanlang"</string>
<string name="screen_login_error_deactivated_account">"Bu hisob ochirilgan."</string>
<string name="screen_login_error_invalid_credentials">"Notog\'ri foydalanuvchi nomi va/yoki parol"</string>
<string name="screen_login_error_invalid_user_id">"Bu haqiqiy foydalanuvchi identifikatori emas. Kutilayotgan format: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"Tanlangan uy serveri parol yoki OIDC loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang."</string>
<string name="screen_login_form_header">"Tafsilotlaringizni kiriting"</string>
<string name="screen_login_subtitle">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
<string name="screen_login_title">"Qaytib kelganingizdan xursandmiz!"</string>
<string name="screen_login_title_with_homeserver">"Kirish%1$s"</string>
<string name="screen_server_confirmation_change_server">"Hisob provayderini o\'zgartiring"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Element xodimlari uchun shaxsiy server."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
<string name="screen_server_confirmation_message_register">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
<string name="screen_server_confirmation_title_login">"Siz tizimga kirmoqchisiz%1$s"</string>
<string name="screen_server_confirmation_title_register">"Hisob yaratmoqchisiz%1$s"</string>
<string name="screen_waitlist_message">"Hozirgi paytda %2$sga %1$sda talab yuqori. Bir necha kundan keyin ilovaga qayting va qaytadan urining.
Sabr-toqatingiz uchun rahmat!"</string>
<string name="screen_waitlist_message_success">"%1$sga Xush kelibsiz!"</string>
<string name="screen_waitlist_title">"Siz deyarli keldingiz."</string>
<string name="screen_waitlist_title_success">"Siz kirdingiz."</string>
</resources>

View file

@ -13,12 +13,12 @@
<string name="screen_change_account_provider_other">"其他"</string> <string name="screen_change_account_provider_other">"其他"</string>
<string name="screen_change_account_provider_subtitle">"使用其他帐户提供者,例如您自己的私人服务器或工作帐户。"</string> <string name="screen_change_account_provider_subtitle">"使用其他帐户提供者,例如您自己的私人服务器或工作帐户。"</string>
<string name="screen_change_account_provider_title">"更改账户提供者"</string> <string name="screen_change_account_provider_title">"更改账户提供者"</string>
<string name="screen_change_server_error_invalid_homeserver">"我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。"</string> <string name="screen_change_server_error_invalid_homeserver">"我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。"</string>
<string name="screen_change_server_error_invalid_well_known">"由于 Well Known 文件中的问题Sliding Sync 不可用: <string name="screen_change_server_error_invalid_well_known">"由于 Well Known 文件中的问题Sliding Sync 不可用:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"该服务器目前不支持sliding sync。"</string> <string name="screen_change_server_error_no_sliding_sync_message">"该服务器目前不支持 Sliding Sync。"</string>
<string name="screen_change_server_form_header">"服务器网址"</string> <string name="screen_change_server_form_header">"服务器网址"</string>
<string name="screen_change_server_form_notice">"您只能连接到支持sliding sync的现有服务器。您的主服务器管理员需要对其进行配置。%1$s"</string> <string name="screen_change_server_form_notice">"您只能连接到支持 Sliding Sync 的现有服务器。您的服务器管理员需要对其进行配置。%1$s"</string>
<string name="screen_change_server_subtitle">"您的服务器地址是什么?"</string> <string name="screen_change_server_subtitle">"您的服务器地址是什么?"</string>
<string name="screen_change_server_title">"选择服务器"</string> <string name="screen_change_server_title">"选择服务器"</string>
<string name="screen_login_error_deactivated_account">"该账户已被停用。"</string> <string name="screen_login_error_deactivated_account">"该账户已被停用。"</string>
@ -34,11 +34,13 @@
<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">"如果这是网络问题,请尝试使用二维码再次登录"</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_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_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_device_not_signed_in_scan_state_description">"在其他设备登录后重试,或使用另一个已登录的设备。"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"其他设备未登录"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"登录被另一台设备取消"</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_cancelled_title">"登录请求已取消"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"其它设备未接受请求"</string> <string name="screen_qr_code_login_error_declined_subtitle">"其它设备未接受请求"</string>
@ -74,8 +76,8 @@
<string name="screen_server_confirmation_message_login_element_dot_io">"专为 Element 员工提供的私人服务器。"</string> <string name="screen_server_confirmation_message_login_element_dot_io">"专为 Element 员工提供的私人服务器。"</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix 是一个用于安全、去中心化通信的开放网络。"</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix 是一个用于安全、去中心化通信的开放网络。"</string>
<string name="screen_server_confirmation_message_register">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string> <string name="screen_server_confirmation_message_register">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string>
<string name="screen_server_confirmation_title_login">"即将登录 %1$s"</string> <string name="screen_server_confirmation_title_login">"即将登录 %1$s"</string>
<string name="screen_server_confirmation_title_register">"即将在 %1$s 上创建一个账户"</string> <string name="screen_server_confirmation_title_register">"即将在 %1$s 上创建一个账户"</string>
<string name="screen_waitlist_message">"目前 %1$s 上 %2$s 的负载很大。过几天再回来试试吧。 <string name="screen_waitlist_message">"目前 %1$s 上 %2$s 的负载很大。过几天再回来试试吧。
感谢您的耐心!"</string> 感谢您的耐心!"</string>

View file

@ -20,10 +20,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -31,6 +29,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -274,7 +274,7 @@ class ConfirmAccountProviderPresenterTest {
params = params, params = params,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
authenticationService = matrixAuthenticationService, authenticationService = matrixAuthenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, oidcActionFlow = defaultOidcActionFlow,
defaultLoginUserStory = defaultLoginUserStory, defaultLoginUserStory = defaultLoginUserStory,
) )
} }

View file

@ -0,0 +1,33 @@
/*
* 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
*
* https://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.features.logout.api
/**
* Used to trigger a log out of the current user from any part of the app.
*/
interface LogoutUseCase {
/**
* Log out the current user and then perform any needed cleanup tasks.
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
* @return the session id of the logged out user.
*/
suspend fun logout(ignoreSdkError: Boolean): String
interface Factory {
fun create(sessionId: String): LogoutUseCase
}
}

View file

@ -0,0 +1,43 @@
/*
* 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
*
* https://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.features.logout.impl
import com.squareup.anvil.annotations.ContributesBinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
class DefaultLogoutUseCase @AssistedInject constructor(
@Assisted private val sessionId: String,
private val matrixClientProvider: MatrixClientProvider,
) : LogoutUseCase {
@ContributesBinding(AppScope::class)
@AssistedFactory
interface Factory : LogoutUseCase.Factory {
override fun create(sessionId: String): DefaultLogoutUseCase
}
override suspend fun logout(ignoreSdkError: Boolean): String {
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
matrixClient.logout(ignoreSdkError = ignoreSdkError)
return sessionId
}
}

View file

@ -0,0 +1,36 @@
/*
* 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
*
* https://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.features.logout.impl
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
@Module
@ContributesTo(SessionScope::class)
object SessionLogoutModule {
@Provides
fun provideLogoutUseCase(
currentSessionIdHolder: CurrentSessionIdHolder,
factory: DefaultLogoutUseCase.Factory,
): LogoutUseCase {
return factory.create(currentSessionIdHolder.current.value)
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Weet je zeker dat je je wilt uitloggen?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Uitloggen"</string>
<string name="screen_signout_confirmation_dialog_title">"Uitloggen"</string>
<string name="screen_signout_in_progress_dialog_content">"Uitloggen…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."</string>
<string name="screen_signout_key_backup_disabled_title">"Je hebt de back-up uitgeschakeld"</string>
<string name="screen_signout_key_backup_offline_subtitle">"De backup van je sleutels was nog bezig toen je offline ging. Maak opnieuw verbinding zodat er een back-up van je sleutels kan worden gemaakt voordat je uitlogt."</string>
<string name="screen_signout_key_backup_offline_title">"De backup van je sleutels is nog bezig"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Wacht tot dit voltooid is voordat je uitlogt."</string>
<string name="screen_signout_key_backup_ongoing_title">"De backup van je sleutels is nog bezig"</string>
<string name="screen_signout_preference_item">"Uitloggen"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."</string>
<string name="screen_signout_recovery_disabled_title">"Herstelmogelijkheid niet ingesteld"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, kan het dat je de toegang tot je versleutelde berichten verliest."</string>
<string name="screen_signout_save_recovery_key_title">"Heb je je herstelsleutel opgeslagen?"</string>
</resources>

View file

@ -1,8 +1,8 @@
<?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_signout_confirmation_dialog_content">"Czy na pewno chcesz się wylogować?"</string> <string name="screen_signout_confirmation_dialog_content">"Czy na pewno chcesz się wylogować?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Wyloguj się"</string> <string name="screen_signout_confirmation_dialog_submit">"Wyloguj"</string>
<string name="screen_signout_confirmation_dialog_title">"Wyloguj się"</string> <string name="screen_signout_confirmation_dialog_title">"Wyloguj"</string>
<string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string> <string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string> <string name="screen_signout_key_backup_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_key_backup_disabled_title">"Wyłączyłeś backup"</string> <string name="screen_signout_key_backup_disabled_title">"Wyłączyłeś backup"</string>
@ -10,7 +10,7 @@
<string name="screen_signout_key_backup_offline_title">"Twoje klucze są nadal archiwizowane"</string> <string name="screen_signout_key_backup_offline_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Zanim się wylogujesz, poczekaj na zakończenie operacji."</string> <string name="screen_signout_key_backup_ongoing_subtitle">"Zanim się wylogujesz, poczekaj na zakończenie operacji."</string>
<string name="screen_signout_key_backup_ongoing_title">"Twoje klucze są nadal archiwizowane"</string> <string name="screen_signout_key_backup_ongoing_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_preference_item">"Wyloguj się"</string> <string name="screen_signout_preference_item">"Wyloguj"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string> <string name="screen_signout_recovery_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_recovery_disabled_title">"Nie ustawiono przywracania"</string> <string name="screen_signout_recovery_disabled_title">"Nie ustawiono przywracania"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string> <string name="screen_signout_save_recovery_key_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>

View file

@ -4,5 +4,9 @@
<string name="screen_signout_confirmation_dialog_submit">"Sair"</string> <string name="screen_signout_confirmation_dialog_submit">"Sair"</string>
<string name="screen_signout_confirmation_dialog_title">"Sair"</string> <string name="screen_signout_confirmation_dialog_title">"Sair"</string>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string> <string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
<string name="screen_signout_key_backup_disabled_title">"Você desativou o backup"</string>
<string name="screen_signout_key_backup_ongoing_title">"O backup das suas chaves ainda está em andamento"</string>
<string name="screen_signout_preference_item">"Sair"</string> <string name="screen_signout_preference_item">"Sair"</string>
<string name="screen_signout_recovery_disabled_title">"A recuperação não está configurada"</string>
<string name="screen_signout_save_recovery_key_title">"Você salvou sua chave de recuperação?"</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show more