Merge branch 'release/25.05.0'

This commit is contained in:
Jorge Martín
2025-05-06 15:58:17 +02:00
713 changed files with 10115 additions and 5128 deletions

View File

@@ -69,7 +69,7 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- uses: rnkdsh/action-upload-diawi@v1.5.8
- uses: rnkdsh/action-upload-diawi@605adbad0db6c000eee26adfd8fc128d7df8f7ab # v1.5.9
id: diawi
# Do not fail the whole build if Diawi upload fails
continue-on-error: true
@@ -81,7 +81,7 @@ jobs:
file: app/build/outputs/apk/gplay/debug/app-gplay-arm64-v8a-debug.apk
- name: Add or update PR comment with QR Code to download APK.
if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
uses: NejcZdovc/comment-pr@v2
uses: NejcZdovc/comment-pr@a423635d183a8259308e80593c96fecf31539c26 # v2.1.0
with:
message: |
:iphone: Scan the QR code below to install the build (arm64 only) for this PR.

View File

@@ -33,7 +33,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
@@ -20,7 +20,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@13.0.4
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:

View File

@@ -12,7 +12,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.3
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v4
with:
@@ -32,7 +32,7 @@ jobs:
mkdir -p screenshots/en
cp tests/uitests/src/test/snapshots/images/* screenshots/en
- name: Deploy GitHub Pages
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./screenshots

View File

@@ -19,7 +19,7 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2
uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0
with:
repo-token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
target-branch: develop

View File

@@ -18,7 +18,7 @@ jobs:
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.3
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v4

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -77,7 +77,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -117,7 +117,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -161,7 +161,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -201,7 +201,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -241,7 +241,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -294,7 +294,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@13.0.4
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:

View File

@@ -19,18 +19,18 @@ jobs:
steps:
- name: Remove Record-Screenshots label
if: github.event.label.name == 'Record-Screenshots'
uses: actions-ecosystem/action-remove-labels@v1
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
with:
labels: Record-Screenshots
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
uses: nschloe/action-cached-lfs-checkout@v1.2.3
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@v1.2.3
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
with:
persist-credentials: false
- name: ☕️ Use JDK 21

View File

@@ -54,7 +54,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}

View File

@@ -36,7 +36,7 @@ jobs:
./tools/localazy/importSupportedLocalesFromLocalazy.py
./tools/test/generateAllScreenshots.py
- name: Create Pull Request for Strings
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
commit-message: Sync Strings from Localazy

View File

@@ -23,7 +23,7 @@ jobs:
- name: Run SAS String script
run: ./tools/sas/import_sas_strings.py
- name: Create Pull Request for SAS Strings
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: Sync SAS Strings
title: Sync SAS Strings

View File

@@ -33,13 +33,13 @@ jobs:
sudo swapon /mnt/swapfile
sudo swapon --show
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.3
uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@v0.9.1
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
@@ -82,7 +82,7 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -8,7 +8,7 @@ jobs:
triage-new-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/91
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -12,7 +12,7 @@ jobs:
if: >
github.repository == 'element-hq/element-x-android'
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/43
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -21,7 +21,7 @@ jobs:
name: Move triaged needs info issues on board
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
id: addItem
with:
project-url: https://github.com/orgs/element-hq/projects/91
@@ -29,7 +29,7 @@ jobs:
labeled: X-Needs-Info
- name: Print itemId
run: echo ${{ steps.addItem.outputs.itemId }}
- uses: kalgurn/update-project-item-status@main
- uses: kalgurn/update-project-item-status@31e54df46a2cdaef4f85c31ac839fbcd2fd7c3a2 # 0.0.3
if: ${{ steps.addItem.outputs.itemId }}
with:
project-url: https://github.com/orgs/element-hq/projects/91
@@ -43,7 +43,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/73
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -54,7 +54,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/57
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -66,7 +66,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'Team: QA') ||
contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/69
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -77,7 +77,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/89
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: nschloe/action-cached-lfs-checkout@v1.2.3
- uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3
- run: |
./tools/git/validate_lfs.sh

View File

@@ -1,3 +1,44 @@
Changes in Element X v25.04.3
=============================
### 🙌 Improvements
* Use PreferenceDropdown for appearance by @ganfra in https://github.com/element-hq/element-x-android/pull/4581
### 🐛 Bugfixes
* Use in-call volume and mode for EC by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4481
* Send SVG images as files by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4595
* Fetch the initial ignored user list manually when subscribing by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4598
* Fix audio output selection for Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/4602
* [a11y] Make more items focusable by @bmarty in https://github.com/element-hq/element-x-android/pull/4605
* Fix ringing calls not stopping when the other user cancels the call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4613
* Ensure that pinning an event makes the pinned messages banner appear by @bmarty in https://github.com/element-hq/element-x-android/pull/4606
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4590
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4612
### 📄 Documentation
* Improve onboarding docs: by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4578
### Dependency upgrades
* Upgrade Rust bindings to `v25.04.11` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4580
* fix(deps): update dependency androidx.sqlite:sqlite-ktx to v2.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4568
* fix(deps): update dependency app.cash.molecule:molecule-runtime to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4585
* fix(deps): update core to v1.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4564
* Upate datastore to 1.1.4 by @bmarty in https://github.com/element-hq/element-x-android/pull/4551
* fix(deps): update media3 to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4592
* chore(deps): update danger/danger-js action to v13 by @renovate in https://github.com/element-hq/element-x-android/pull/4596
* fix(deps): update dependency io.element.android:emojibase-bindings to v1.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4591
* fix(deps): update dagger to v2.56.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4603
* fix(deps): update dependency io.sentry:sentry-android to v8.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4557
* fix(deps): update dependency androidx.compose:compose-bom to v2025.04.00 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/4565
* fix(deps): update dependency com.posthog:posthog-android to v3.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4616
* fix(deps): update android.gradle.plugin to v8.9.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4615
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.22 by @renovate in https://github.com/element-hq/element-x-android/pull/4622
### Others
* Improve accessibility of the timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4579
* Push: improve Push history screen, log and stored data by @bmarty in https://github.com/element-hq/element-x-android/pull/4601
* Push gateway config by @bmarty in https://github.com/element-hq/element-x-android/pull/4608
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.04.2...v25.04.3
Changes in Element X v25.04.2
=============================

View File

@@ -106,14 +106,25 @@ android {
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName)")
buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
getByName("debug") {
resValue("string", "app_name", "$baseAppName dbg")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.debug",
)
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
getByName("release") {
resValue("string", "app_name", baseAppName)
resValue(
"string",
"login_redirect_scheme",
oidcRedirectSchemeBase,
)
signingConfig = signingConfigs.getByName("debug")
postprocessing {
@@ -131,6 +142,11 @@ android {
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "$baseAppName nightly")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.nightly",
)
matchingFallbacks += listOf("release")
signingConfig = signingConfigs.getByName("nightly")
@@ -284,6 +300,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)
koverDependencies()
}

View File

@@ -60,7 +60,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="io.element" />
<data android:scheme="@string/login_redirect_scheme" />
</intent-filter>
<!--
Element web links

View File

@@ -10,14 +10,17 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appnav.di.RoomComponentFactory
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultRoomComponentFactory @Inject constructor(
private val roomComponentBuilder: RoomComponent.Builder
) : RoomComponentFactory {
override fun create(room: MatrixRoom): Any {
return roomComponentBuilder.room(room).build()
override fun create(room: JoinedRoom): Any {
return roomComponentBuilder
.joinedRoom(room)
.baseRoom(room)
.build()
}
}

View File

@@ -14,7 +14,8 @@ import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
@SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class)
@@ -22,7 +23,11 @@ interface RoomComponent : NodeFactoriesBindings {
@MergeSubcomponent.Builder
interface Builder {
@BindsInstance
fun room(room: MatrixRoom): Builder
fun joinedRoom(room: JoinedRoom): Builder
@BindsInstance
fun baseRoom(room: BaseRoom): Builder
fun build(): RoomComponent
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.oidc
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.x.R
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultOidcRedirectUrlProvider @Inject constructor(
private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider {
override fun provide() = buildString {
append(stringProvider.getString(R.string.login_redirect_scheme))
append(":/")
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.oidc
import com.google.common.truth.Truth.assertThat
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.x.R
import org.junit.Test
class DefaultOidcRedirectUrlProviderTest {
@Test
fun `test provide`() {
val stringProvider = FakeStringProvider(
defaultResult = "str"
)
val sut = DefaultOidcRedirectUrlProvider(
stringProvider = stringProvider,
)
val result = sut.provide()
assertThat(result).isEqualTo("str:/")
assertThat(stringProvider.lastResIdParam).isEqualTo(R.string.login_redirect_scheme)
}
}

View File

@@ -10,4 +10,7 @@ package io.element.android.appconfig
object MatrixConfiguration {
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
val clientPermalinkBaseUrl: String? = null
// TODO remove this when report is fixed
const val CAN_REPORT_ROOM = false
}

View File

@@ -7,8 +7,8 @@
package io.element.android.appnav.di
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface RoomComponentFactory {
fun create(room: MatrixRoom): Any
fun create(room: JoinedRoom): Any
}

View File

@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
@VisibleForTesting
@@ -42,7 +43,9 @@ class SendQueues @Inject constructor(
) { syncState, _ -> syncState }
.debounce(SEND_QUEUES_RETRY_DELAY_MILLIS)
.onEach { syncState ->
Timber.tag("SendQueues").d("Sync state changed: $syncState")
if (syncState == SyncState.Running) {
Timber.tag("SendQueues").d("Enabling send queues again")
matrixClient.setAllSendQueuesEnabled(enabled = true)
}
}

View File

@@ -28,7 +28,6 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.joined.LoadingRoomNodeView
import io.element.android.appnav.room.joined.LoadingRoomState
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
@@ -49,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first

View File

@@ -36,6 +36,8 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map

View File

@@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -72,7 +72,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
}
data class Inputs(
val room: MatrixRoom,
val room: JoinedRoom,
val initialElement: RoomNavigationTarget,
) : NodeInputs
@@ -95,6 +95,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
},
onDestroy = {
Timber.v("OnDestroy")
inputs.room.destroy()
appNavigationStateService.onLeavingRoom(id)
}
)

View File

@@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
import io.element.android.libraries.ui.strings.CommonStrings
@Composable

View File

@@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Allgofnodi ac Uwchraddio"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"Nid yw %1$s bellach yn cefnogi\'r hen brotocol. Allgofnodwch a mewngofnodi\'n ôl i barhau i ddefnyddio\'r ap."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Nid yw eich gweinydd cartref yn cefnogi\'r hen brotocol mwyach. Allgofnodwch a mewngofnodwch yn ôl i barhau i ddefnyddio\'r ap."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Nid yw eich gweinydd cartref yn cefnogi\'r hen brotocol mwyach. Allgofnodwch a mewngofnodi yn ôl i barhau i ddefnyddio\'r ap."</string>
</resources>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"Το %1$s δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδεθείτε και συνδεθείτε ξανά για να συνεχίσετε να χρησιμοποιείτε την εφαρμογή."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
</resources>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"登出并升级"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s 不再支持旧协议。请注销并重新登录以继续使用该应用程序。"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。"</string>
</resources>

View File

@@ -23,16 +23,17 @@ import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.childNode
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class JoinRoomLoadedFlowNodeTest {
class JoinBaseRoomLoadedFlowNodeTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@@ -68,7 +69,7 @@ class JoinRoomLoadedFlowNodeTest {
}
private class FakeRoomComponentFactory : RoomComponentFactory {
override fun create(room: MatrixRoom): Any {
override fun create(room: JoinedRoom): Any {
return Unit
}
}
@@ -114,9 +115,7 @@ class JoinRoomLoadedFlowNodeTest {
@Test
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
// GIVEN
val room = FakeMatrixRoom(
updateMembersResult = { }
)
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val roomFlowNode = createJoinedRoomLoadedFlowNode(
@@ -137,9 +136,7 @@ class JoinRoomLoadedFlowNodeTest {
@Test
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
// GIVEN
val room = FakeMatrixRoom(
updateMembersResult = { }
)
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())

View File

@@ -20,10 +20,11 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
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.libraries.oidc.impl.DefaultOidcUrlParser
import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -119,7 +120,7 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
@@ -134,13 +135,13 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
oidcAction = OidcAction.Success(
url = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
)
)
)
@@ -151,7 +152,7 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback/invalid".toUri()
data = "io.element.android:/invalid".toUri()
}
assertThrows(IllegalStateException::class.java) {
sut.resolve(intent)
@@ -246,7 +247,9 @@ class IntentResolverTest {
return IntentResolver(
deeplinkParser = DeeplinkParser(),
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
oidcUrlParser = DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
)
),
permalinkParser = FakePermalinkParser(
result = permalinkParserResult

View File

@@ -10,7 +10,7 @@ package io.element.android.appnav.loggedin
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -35,8 +35,8 @@ class SendQueuesTest {
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
val room = FakeMatrixRoom(
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
val room = FakeJoinedRoom(
setSendQueueEnabledResult = setRoomSendQueueEnabledLambda
)
matrixClient.givenGetRoomResult(room.roomId, room)
sut.launchIn(backgroundScope)
@@ -61,8 +61,8 @@ class SendQueuesTest {
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
syncService.emitSyncState(SyncState.Offline)
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
val room = FakeMatrixRoom(
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
val room = FakeJoinedRoom(
setSendQueueEnabledResult = setRoomSendQueueEnabledLambda
)
matrixClient.givenGetRoomResult(room.roomId, room)

View File

@@ -9,21 +9,22 @@ package io.element.android.appnav.room
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.room.joined.LoadingRoomState
import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory
import io.element.android.libraries.matrix.api.roomlist.RoomList
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LoadingRoomStateFlowFactoryTest {
class LoadingBaseRoomStateFlowFactoryTest {
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
val matrixClient = FakeMatrixClient(A_SESSION_ID).apply {
givenGetRoomResult(A_ROOM_ID, room)
}
@@ -38,7 +39,7 @@ class LoadingRoomStateFlowFactoryTest {
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)

View File

@@ -11,7 +11,7 @@ Server list: https://github.com/element-hq/oidc-playground
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
clientName: InfoPlistReader.main.bundleDisplayName,
redirectUri: "io.element:/callback",
redirectUri: "io.element.android:/",
clientUri: "https://element.io",
tosUri: "https://element.io/user-terms-of-service",
policyUri: "https://element.io/privacy"
@@ -19,7 +19,7 @@ policyUri: "https://element.io/privacy"
Android:
clientName = "Element",
redirectUri = "io.element:/callback",
redirectUri = "io.element.android:/",
clientUri = "https://element.io",
tosUri = "https://element.io/user-terms-of-service",
policyUri = "https://element.io/privacy"

View File

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

View File

@@ -1,4 +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">"Rhannu data defnydd dienw i\'n helpu i nodi problemau."</string>
<string name="screen_analytics_settings_read_terms">"Gallwch ddarllen ein holl amodau %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"yma"</string>
<string name="screen_analytics_settings_share_data">"Rhannu data dadansoddeg"</string>
</resources>

View File

@@ -1,7 +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">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s ."</string>
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
</resources>

View File

@@ -1,7 +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">"Partekatu erabilerari buruzko datu anonimoak arazoak identifikatzen laguntzeko."</string>
<string name="screen_analytics_settings_read_terms">"Gure baldintza guztiak irakur ditzakezu %1$s ."</string>
<string name="screen_analytics_settings_read_terms">"Gure baldintza guztiak irakur ditzakezu %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hemen"</string>
<string name="screen_analytics_settings_share_data">"Partekatu analisi-datuak"</string>
</resources>

View File

@@ -1,7 +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">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_settings_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s ."</string>
<string name="screen_analytics_settings_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"čia"</string>
<string name="screen_analytics_settings_share_data">"Dalytis analitiniais duomenimis"</string>
</resources>

View File

@@ -1,7 +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">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_settings_share_data">"Compartilhar dados de utilização"</string>
</resources>

View File

@@ -1,4 +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">"Fyddwn ni ddim yn cofnodi nac yn proffilio unrhyw ddata personol"</string>
<string name="screen_analytics_prompt_help_us_improve">"Rhannu data defnydd dienw i\'n helpu i nodi problemau."</string>
<string name="screen_analytics_prompt_read_terms">"Gallwch ddarllen ein holl amodau %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"yma"</string>
<string name="screen_analytics_prompt_settings">"Gallwch ddiffodd hwn unrhyw bryd"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Fyddwn ni ddim yn rhannu eich data gyda thrydydd parti"</string>
<string name="screen_analytics_prompt_title">"Helpwch i wella %1$s"</string>
</resources>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s ."</string>
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_help_us_improve">"Partekatu erabilerari buruzko datu anonimoak arazoak identifikatzen laguntzeko."</string>
<string name="screen_analytics_prompt_read_terms">"Gure baldintza guztiak irakur ditzakezu %1$s ."</string>
<string name="screen_analytics_prompt_read_terms">"Gure baldintza guztiak irakur ditzakezu %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hemen"</string>
<string name="screen_analytics_prompt_settings">"Edozein unetan desaktibatu dezakezu"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Ez ditugu zure datuak hirugarrenekin partekatuko"</string>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Mes nekaupsime ir neprofiliuosime jokių asmens duomenų"</string>
<string name="screen_analytics_prompt_help_us_improve">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s ."</string>
<string name="screen_analytics_prompt_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"čia"</string>
<string name="screen_analytics_prompt_settings">"Tai galite bet kada išjungti"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis"</string>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Não registraremos nem criaremos perfil baseado em qualquer dado pessoal"</string>
<string name="screen_analytics_prompt_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
<string name="screen_analytics_prompt_read_terms">"Você pode ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_prompt_settings">"Você pode desativar isso a qualquer momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Não compartilharemos seus dados com terceiros"</string>

View File

@@ -241,7 +241,7 @@ class CallScreenPresenter @AssistedInject constructor(
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
if (!notifiedCallStart) {
getRoom(roomId)?.sendCallNotificationIfNeeded()
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
?.onSuccess { notifiedCallStart = true }
}
}

View File

@@ -10,7 +10,6 @@ package io.element.android.features.call.impl.ui
import android.Manifest
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.res.Configuration
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
@@ -78,7 +77,6 @@ class ElementCallActivity :
private val requestPermissionsLauncher = registerPermissionResultLauncher()
private var isDarkMode = false
private val webViewTarget = mutableStateOf<CallType?>(null)
private var eventSink: ((CallScreenEvents) -> Unit)? = null
@@ -102,10 +100,6 @@ class ElementCallActivity :
return
}
if (savedInstanceState == null) {
updateUiMode(resources.configuration)
}
pictureInPicturePresenter.setPipView(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
@@ -174,11 +168,6 @@ class ElementCallActivity :
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateUiMode(newConfig)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setCallType(intent)
@@ -283,19 +272,6 @@ class ElementCallActivity :
}
}
private fun updateUiMode(configuration: Configuration) {
val prevDarkMode = isDarkMode
val currentNightMode = configuration.uiMode and Configuration.UI_MODE_NIGHT_YES
isDarkMode = currentNightMode != 0
if (prevDarkMode != isDarkMode) {
if (isDarkMode) {
window.setBackgroundDrawableResource(android.R.drawable.screen_background_dark)
} else {
window.setBackgroundDrawableResource(android.R.drawable.screen_background_light)
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
override fun setPipParams() {
setPictureInPictureParams(getPictureInPictureParams())

View File

@@ -13,6 +13,8 @@ import android.os.PowerManager
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.call.api.CallType
@@ -23,6 +25,8 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
@@ -89,6 +93,7 @@ class DefaultActiveCallManager @Inject constructor(
private val matrixClientProvider: MatrixClientProvider,
private val defaultCurrentCallService: DefaultCurrentCallService,
private val appForegroundStateService: AppForegroundStateService,
private val imageLoaderHolder: ImageLoaderHolder,
) : ActiveCallManager {
private val tag = "DefaultActiveCallManager"
private var timedOutCallJob: Job? = null
@@ -125,6 +130,7 @@ class DefaultActiveCallManager @Inject constructor(
)
timedOutCallJob = coroutineScope.launch {
setUpCoil(notificationData.sessionId)
showIncomingCallNotification(notificationData)
// Wait for the ringing call to time out
@@ -140,6 +146,13 @@ class DefaultActiveCallManager @Inject constructor(
}
}
@OptIn(DelicateCoilApi::class)
private suspend fun setUpCoil(sessionId: SessionId) {
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return
// Ensure that the image loader is set, else the IncomingCallActivity will not be able to render the caller avatar
SingletonImageLoader.setUnsafe(imageLoaderHolder.get(matrixClient))
}
/**
* Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification.
*/

View File

@@ -33,7 +33,7 @@ class DefaultCallWidgetProvider @Inject constructor(
theme: String?,
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
val room = matrixClient.getRoom(roomId) ?: error("Room not found")
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
@@ -47,8 +47,11 @@ class DefaultCallWidgetProvider @Inject constructor(
theme = theme,
).getOrThrow()
val driver = room.getWidgetDriver(widgetSettings).getOrThrow()
room.destroy()
CallWidgetProvider.GetWidgetResult(
driver = room.getWidgetDriver(widgetSettings).getOrThrow(),
driver = driver,
url = callUrl,
)
}

View File

@@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"Galwad cyfredol"</string>
<string name="call_foreground_service_message_android">"Tapio i ddychwelyd i\'r alwad"</string>
<string name="call_foreground_service_title_android">"☎️ Galwad ar y gweill"</string>
<string name="screen_incoming_call_subtitle_android">"Galwad Element"</string>
</resources>

View File

@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
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.FakeJoinedRoom
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
@@ -84,7 +84,7 @@ import kotlin.time.Duration.Companion.seconds
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 syncService = FakeSyncService(SyncState.Running)
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val client = FakeMatrixClient(syncService = syncService).apply {
givenGetRoomResult(A_ROOM_ID, fakeRoom)
}

View File

@@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2
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.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@@ -227,7 +227,7 @@ class DefaultActiveCallManagerTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `observeRingingCalls - will cancel the active ringing call if the call is cancelled`() = runTest {
val room = FakeMatrixRoom().apply {
val room = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val client = FakeMatrixClient().apply {
@@ -254,7 +254,7 @@ class DefaultActiveCallManagerTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest {
val room = FakeMatrixRoom().apply {
val room = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val client = FakeMatrixClient().apply {
@@ -325,5 +325,6 @@ class DefaultActiveCallManagerTest {
matrixClientProvider = matrixClientProvider,
defaultCurrentCallService = DefaultCurrentCallService(),
appForegroundStateService = FakeAppForegroundStateService(),
imageLoaderHolder = FakeImageLoaderHolder(),
)
}

View File

@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
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.FakeJoinedRoom
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -41,7 +41,7 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if it can't generate the URL for the widget`() = runTest {
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.failure(Exception("Can't generate URL for widget")) }
)
val client = FakeMatrixClient().apply {
@@ -53,7 +53,7 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if it can't get the widget driver`() = runTest {
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.failure(Exception("Can't get a widget driver")) }
)
@@ -66,7 +66,7 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)
@@ -79,7 +79,7 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeMatrixRoom(
val room = FakeJoinedRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)

View File

@@ -3,9 +3,10 @@
<string name="screen_create_room_action_create_room">"Новы пакой"</string>
<string name="screen_create_room_add_people_title">"Запрасіць карыстальнікаў"</string>
<string name="screen_create_room_error_creating_room">"Пры стварэнні пакоя адбылася памылка"</string>
<string name="screen_create_room_private_option_description">"Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць."</string>
<string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</string>
<string name="screen_create_room_public_option_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string>
<string name="screen_create_room_private_option_description">"Толькі запрошаныя людзі могуць атрымаць доступ да гэтага пакоя. Усе паведамленні абаронены end-to-end шыфраваннем."</string>
<string name="screen_create_room_private_option_title">"Прыватны пакой"</string>
<string name="screen_create_room_public_option_description">"Любы можа знайсці гэты пакой.
Вы можаце змяніць гэта ў любы час у наладах пакоя."</string>
<string name="screen_create_room_public_option_title">"Публічны пакой"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Хто заўгодна"</string>
<string name="screen_create_room_room_access_section_header">"Доступ у пакой"</string>

View File

@@ -1,6 +1,13 @@
<?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">"Ystafell newydd"</string>
<string name="screen_create_room_add_people_title">"Gwahodd pobl"</string>
<string name="screen_create_room_error_creating_room">"Bu gwall wrth greu\'r ystafell"</string>
<string name="screen_create_room_private_option_description">"Dim ond pobl wahoddwyd all gael mynediad i\'r ystafell hon. Mae pob neges wedi\'i hamgryptio o\'r dechrau i\'r diwedd."</string>
<string name="screen_create_room_private_option_title">"Ystafell breifat"</string>
<string name="screen_create_room_public_option_description">"Gall unrhyw un ddod o hyd i\'r ystafell hon.
Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."</string>
<string name="screen_create_room_public_option_title">"Ystafell gyhoeddus"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Gall unrhyw un ymuno â\'r ystafell hon"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Unrhyw un"</string>
<string name="screen_create_room_room_access_section_header">"Mynediad i\'r Ystafell"</string>
@@ -11,4 +18,13 @@
<string name="screen_create_room_room_name_label">"Enw\'r ystafell"</string>
<string name="screen_create_room_room_visibility_section_title">"Gwelededd yr ystafell"</string>
<string name="screen_create_room_title">"Creu ystafell"</string>
<string name="screen_create_room_topic_label">"Pwnc (dewisol)"</string>
<string name="screen_room_directory_search_title">"Cyfeiriadur ystafelloedd"</string>
<string name="screen_start_chat_error_starting_chat">"Digwyddodd gwall wrth geisio cychwyn sgwrs"</string>
<string name="screen_start_chat_join_room_by_address_action">"Ymuno â\'r ystafell yn ôl cyfeiriad"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ddim yn gyfeiriad dilys"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Ewch i mewn…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Cafwyd hyd i ystafell gyfatebol"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Heb ganfod yr ystafell"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"e.e. #enw-ystafell:matrix.org"</string>
</resources>

View File

@@ -14,10 +14,17 @@
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση δωματίου"</string>
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα δωματίου"</string>
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
<string name="screen_room_directory_search_title">"Κατάλογος δωματίων"</string>
<string name="screen_start_chat_error_starting_chat">"Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"</string>
<string name="screen_start_chat_join_room_by_address_action">"Συμμετοχή σε δωμάτιο μέσω διεύθυνσης"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Μη έγκυρη διεύθυνση"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Εισάγετε…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Βρέθηκε το αντίστοιχο δωμάτιο"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Το δωμάτιο δε βρέθηκε"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"π.χ. #όνομα-δωματίου:matrix.org"</string>
</resources>

View File

@@ -3,12 +3,25 @@
<string name="screen_create_room_action_create_room">"اتاق جدید"</string>
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
<string name="screen_create_room_private_option_description">"پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند."</string>
<string name="screen_create_room_private_option_title">"اتاق خصوصی (فقط دعوت)"</string>
<string name="screen_create_room_public_option_description">"پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید."</string>
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده می‌توانند به این اتاق دسترسی داشته باشند. همهٔ پیام‌ها رمزنگاری سرتاسری شده‌اند."</string>
<string name="screen_create_room_private_option_title">"اتاق خصوصی"</string>
<string name="screen_create_room_public_option_description">"هرکسی می‌تواند اتاق را بیابد.
می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string>
<string name="screen_create_room_public_option_title">"اتاق عمومی"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"هرکسی"</string>
<string name="screen_create_room_room_access_section_header">"دسترسی اتاق"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
<string name="screen_create_room_room_name_label">"نام اتاق"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_room_directory_search_title">"فهرست اتاق‌ها"</string>
<string name="screen_start_chat_join_room_by_address_action">"پیوستن به اتاق با نشانی"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"نشانی معتبری نیست"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"ورود…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"اتاق مطابق پیدا شد"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"اتاق پیدا نشد"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"نمونه: #room-name:matrix.org"</string>
</resources>

View File

@@ -21,4 +21,10 @@
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
<string name="screen_room_directory_search_title">"聊天室目錄"</string>
<string name="screen_start_chat_error_starting_chat">"嘗試開始聊天時發生錯誤"</string>
<string name="screen_start_chat_join_room_by_address_action">"按地址加入聊天室"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"不是有效的位址"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"輸入……"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"找到相符的聊天室"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"找不到聊天室"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"例如 #room-name:matrix.org"</string>
</resources>

View File

@@ -66,7 +66,7 @@ private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
@RunWith(RobolectricTestRunner::class)
class ConfigureRoomPresenterTest {
class ConfigureBaseRoomPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()

View File

@@ -21,7 +21,7 @@ import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
class JoinRoomByAddressPresenterTest {
class JoinBaseRoomByAddressPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createJoinRoomByAddressPresenter()

View File

@@ -23,7 +23,7 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class JoinRoomByAddressViewTest {
class JoinBaseRoomByAddressViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()

View File

@@ -36,7 +36,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class CreateRoomRootPresenterTest {
class CreateBaseRoomRootPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()

View File

@@ -33,7 +33,7 @@ import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class CreateRoomRootViewTest {
class CreateBaseRoomRootViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()

View File

@@ -1,4 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Cadarnhewch eich bod am gau\'r cyfrif. Does dom modd dadwneud y weithred hon."</string>
<string name="screen_deactivate_account_delete_all_messages">"Dileu fy holl negeseuon"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Rhybudd: Mae\'n bosibl y bydd defnyddwyr y dyfodol yn gweld sgyrsiau anghyflawn."</string>
<string name="screen_deactivate_account_description">"Bydd cau eich cyfrif yn %1$s yn:"</string>
<string name="screen_deactivate_account_description_bold_part">"dim modd ei adfer"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s eich cyfrif (does dim modd i chi fewngofnodi eto, ac nid oes modd ailddefnyddio\'ch dull adnabod)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Analluogi\'n barhaol"</string>
<string name="screen_deactivate_account_list_item_2">"Eich tynnu o bob ystafell sgwrsio."</string>
<string name="screen_deactivate_account_list_item_3">"Dileu manylion eich cyfrif o\'n gweinydd hunaniaeth."</string>
<string name="screen_deactivate_account_list_item_4">"Bydd eich negeseuon yn dal i fod yn weladwy i ddefnyddwyr cofrestredig ond fyddan nhw ddim ar gael i ddefnyddwyr newydd neu anghofrestredig os byddwch yn dewis eu dileu."</string>
<string name="screen_deactivate_account_title">"Cau cyfrif"</string>
</resources>

View File

@@ -2,4 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_description">"Desativar sua conta é %1$s, isso irá:"</string>
<string name="screen_deactivate_account_list_item_2">"Te remover de todas as salas de conversa."</string>
<string name="screen_deactivate_account_title">"Desativar conta"</string>
</resources>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_welcome_button">"Ffwrdd â ni!"</string>
<string name="screen_identity_confirmation_cannot_confirm">"Methu cadarnhau?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crëwch allwedd adfer newydd"</string>
<string name="screen_identity_confirmation_subtitle">"Dilyswch y ddyfais hon er mwyn gosod negeseuon diogel."</string>
<string name="screen_identity_confirmation_title">"Cadarnhewch eich hunaniaeth"</string>
<string name="screen_identity_confirmation_use_another_device">"Defnyddiwch ddyfais arall"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Defnyddiwch allwedd adfer"</string>
<string name="screen_identity_confirmed_subtitle">"Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel, a gall unrhyw un rydych chi\'n sgwrsio â nhw ymddiried yn y ddyfais hon hefyd."</string>
<string name="screen_identity_confirmed_title">"Dyfais wedi\'i dilysu"</string>
<string name="screen_identity_use_another_device">"Defnyddiwch ddyfais arall"</string>
<string name="screen_identity_waiting_on_other_device">"Yn aros ar ddyfais arall…"</string>
<string name="screen_notification_optin_subtitle">"Gallwch newid eich gosodiadau yn nes ymlaen."</string>
<string name="screen_notification_optin_title">"Caniatáu hysbysiadau a pheidio byth â cholli neges"</string>
<string name="screen_session_verification_enter_recovery_key">"Rhowch eich allwedd adfer"</string>
<string name="screen_welcome_bullet_1">"Bydd galwadau, pleidleisiau, chwilio a mwy yn cael eu hychwanegu yn ddiweddarach eleni."</string>
<string name="screen_welcome_bullet_2">"Nid yw hanes negeseuon ar gyfer ystafelloedd sydd wedi\'u hamgryptio ar gael eto."</string>
<string name="screen_welcome_bullet_3">"Byddem wrth ein bodd yn clywed gennych, gadewch i ni wybod beth yw eich barn drwy\'r dudalen gosodiadau."</string>
<string name="screen_welcome_button">"Ymlaen!"</string>
<string name="screen_welcome_subtitle">"Dyma beth sydd angen i chi ei wybod:"</string>
<string name="screen_welcome_title">"Croeso i %1$s!"</string>
</resources>

View File

@@ -7,6 +7,7 @@
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
@@ -16,5 +17,7 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.services.analytics.api)
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import kotlinx.parcelize.Parcelize
@Parcelize
data class InviteData(
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,
) : Parcelable
fun RoomPreviewInfo.toInviteData(): InviteData {
return InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = false,
)
}
fun RoomInfo.toInviteData(): InviteData {
return InviteData(
roomId = id,
roomName = name ?: id.value,
isDm = isDm,
)
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.acceptdecline
import io.element.android.features.invite.api.InviteData
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
}

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
package io.element.android.features.invite.api.acceptdecline
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId

View File

@@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
package io.element.android.features.invite.api.acceptdecline
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState>
@@ -18,27 +18,21 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
anAcceptDeclineInviteState(),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = false, roomName = "Some room", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
blockUser = true,
),
),
anAcceptDeclineInviteState(
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
acceptAction = AsyncAction.Failure(Throwable("Error while accepting invite")),
),
anAcceptDeclineInviteState(
declineAction = AsyncAction.Failure(Throwable("Whoops")),
declineAction = AsyncAction.Failure(Throwable("Error while declining invite")),
),
)
}

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
package io.element.android.features.invite.api.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -15,8 +15,8 @@ interface AcceptDeclineInviteView {
@Composable
fun Render(
state: AcceptDeclineInviteState,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
onAcceptInviteSuccess: (RoomId) -> Unit,
onDeclineInviteSuccess: (RoomId) -> Unit,
modifier: Modifier,
)
}

View File

@@ -5,11 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
package io.element.android.features.invite.api.acceptdecline
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite(
val inviteData: InviteData,
val blockUser: Boolean,
) : AsyncAction.Confirming
data class ConfirmingDeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : AsyncAction.Confirming

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint
interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
}

View File

@@ -1,13 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents
}

View File

@@ -1,18 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.api.response
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class InviteData(
val senderId: UserId,
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,
)

View File

@@ -14,6 +14,11 @@ plugins {
android {
namespace = "io.element.android.features.invite.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupAnvil()
@@ -36,9 +41,12 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.features.invite.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.di.SessionScope
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import javax.inject.Inject
interface AcceptInvite {
suspend operator fun invoke(roomId: RoomId): Result<RoomId>
}
@ContributesBinding(SessionScope::class)
class DefaultAcceptInvite @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : AcceptInvite {
override suspend fun invoke(roomId: RoomId): Result<RoomId> {
return joinRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
).onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
seenInvitesStore.markAsUnSeen(roomId)
}.map { roomId }
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import javax.inject.Inject
interface DeclineInvite {
suspend operator fun invoke(
roomId: RoomId,
blockUser: Boolean,
reportRoom: Boolean,
reportReason: String?
): Result<RoomId>
sealed class Exception : kotlin.Exception() {
data object RoomNotFound : Exception()
data object DeclineInviteFailed : Exception()
data object ReportRoomFailed : Exception()
data object BlockUserFailed : Exception()
}
}
@ContributesBinding(SessionScope::class)
class DefaultDeclineInvite @Inject constructor(
private val client: MatrixClient,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : DeclineInvite {
override suspend fun invoke(
roomId: RoomId,
blockUser: Boolean,
reportRoom: Boolean,
reportReason: String?
): Result<RoomId> {
val room = client.getRoom(roomId) ?: return Result.failure(DeclineInvite.Exception.RoomNotFound)
room.use {
room.leave()
.onFailure { return Result.failure(DeclineInvite.Exception.DeclineInviteFailed) }
.onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(
sessionId = client.sessionId,
roomId = roomId
)
seenInvitesStore.markAsUnSeen(roomId)
}
if (blockUser) {
val userIdToBlock = room.info().inviter?.userId
if (userIdToBlock != null) {
client
.ignoreUser(userIdToBlock)
.onFailure { return Result.failure(DeclineInvite.Exception.BlockUserFailed) }
}
}
if (reportRoom) {
room
.reportRoom(reportReason)
.onFailure { return Result.failure(DeclineInvite.Exception.ReportRoomFailed) }
}
}
return Result.success(roomId)
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AcceptDeclineInvitePresenter @Inject constructor(
private val acceptInvite: AcceptInvite,
private val declineInvite: DeclineInvite,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
val inviteData = event.invite
if (event.shouldConfirm) {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
} else {
localCoroutineScope.declineInvite(
inviteData = inviteData,
blockUser = event.blockUser,
declinedAction = declinedAction,
)
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
}
return AcceptDeclineInviteState(
acceptAction = acceptedAction.value,
declineAction = declinedAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.acceptInvite(
roomId: RoomId,
acceptedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
acceptedAction.runUpdatingState {
acceptInvite(roomId)
}
}
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
declinedAction.runUpdatingState {
declineInvite(
roomId = inviteData.roomId,
blockUser = blockUser,
reportRoom = false,
reportReason = null
)
}
}
}

View File

@@ -5,17 +5,18 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@@ -27,21 +28,21 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AcceptDeclineInviteView(
state: AcceptDeclineInviteState,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
onAcceptInviteSuccess: (RoomId) -> Unit,
onDeclineInviteSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
AsyncActionView(
async = state.acceptAction,
onSuccess = onAcceptInvite,
onSuccess = onAcceptInviteSuccess,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
},
)
AsyncActionView(
async = state.declineAction,
onSuccess = onDeclineInvite,
onSuccess = onDeclineInviteSuccess,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
},
@@ -52,7 +53,13 @@ fun AcceptDeclineInviteView(
invite = confirming.inviteData,
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(
confirming.inviteData,
blockUser = confirming.blockUser,
shouldConfirm = false
)
)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
@@ -72,30 +79,21 @@ private fun DeclineConfirmationDialog(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier
) {
val senderId = invite.senderId.value
val content = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName)
else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName)
}
val title = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title)
else -> stringResource(R.string.screen_invites_decline_chat_title)
}
val submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
}
ConfirmationDialog(
modifier = modifier,
content = content,
title = title,
submitText = submitText,
content = stringResource(R.string.screen_invites_decline_chat_message, invite.roomName),
title = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_title)
} else {
stringResource(R.string.screen_invites_decline_chat_title)
},
submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
},
cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClick = onConfirmClick,
destructiveSubmit = blockUser,
onDismiss = onDismissClick,
)
}
@@ -106,7 +104,7 @@ internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInvit
ElementPreview {
AcceptDeclineInviteView(
state = state,
onAcceptInvite = {},
onDeclineInvite = {},
onAcceptInviteSuccess = {},
onDeclineInviteSuccess = {},
)
}

View File

@@ -5,13 +5,13 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject
@@ -21,14 +21,14 @@ class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInvite
@Composable
override fun Render(
state: AcceptDeclineInviteState,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
onAcceptInviteSuccess: (RoomId) -> Unit,
onDeclineInviteSuccess: (RoomId) -> Unit,
modifier: Modifier,
) {
AcceptDeclineInviteView(
state = state,
onAcceptInvite = onAcceptInvite,
onDeclineInvite = onDeclineInvite,
onAcceptInviteSuccess = onAcceptInviteSuccess,
onDeclineInviteSuccess = onDeclineInviteSuccess,
modifier = modifier
)
}

View File

@@ -5,12 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
sealed interface DeclineAndBlockEvents {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
data object ToggleReportRoom : DeclineAndBlockEvents
data object ToggleBlockUser : DeclineAndBlockEvents
data object Decline : DeclineAndBlockEvents
data object ClearDeclineAction : DeclineAndBlockEvents
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class DeclineAndBlockNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: DeclineAndBlockPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val inviteData: InviteData) : NodeInputs
private val inviteData = inputs<Inputs>().inviteData
private val presenter = presenterFactory.create(inviteData)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
DeclineAndBlockView(
state = state,
onBackClick = ::navigateUp,
modifier = modifier
)
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class DeclineAndBlockPresenter @AssistedInject constructor(
@Assisted private val inviteData: InviteData,
private val declineInvite: DeclineInvite,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<DeclineAndBlockState> {
@AssistedFactory
interface Factory {
fun create(inviteData: InviteData): DeclineAndBlockPresenter
}
@Composable
override fun present(): DeclineAndBlockState {
var reportReason by rememberSaveable { mutableStateOf("") }
var blockUser by rememberSaveable { mutableStateOf(true) }
var reportRoom by rememberSaveable { mutableStateOf(false) }
val declineAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val coroutineScope = rememberCoroutineScope()
fun handleEvents(event: DeclineAndBlockEvents) {
when (event) {
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
}
}
return DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
blockUser = blockUser,
declineAction = declineAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.decline(
reason: String,
blockUser: Boolean,
reportRoom: Boolean,
action: MutableState<AsyncAction<Unit>>
) = launch {
action.value = AsyncAction.Loading
declineInvite(
roomId = inviteData.roomId,
blockUser = blockUser,
reportRoom = reportRoom,
reportReason = reason
).onSuccess {
action.value = AsyncAction.Success(Unit)
}.onFailure { error ->
if (error is DeclineInvite.Exception.DeclineInviteFailed) {
action.value = AsyncAction.Failure(error)
} else {
action.value = AsyncAction.Uninitialized
snackbarDispatcher.post(SnackbarMessage(CommonStrings.error_unknown))
}
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import io.element.android.libraries.architecture.AsyncAction
data class DeclineAndBlockState(
val reportRoom: Boolean,
val reportReason: String,
val blockUser: Boolean,
val declineAction: AsyncAction<Unit>,
val eventSink: (DeclineAndBlockEvents) -> Unit
) {
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
open class DeclineAndBlockStateProvider : PreviewParameterProvider<DeclineAndBlockState> {
override val values: Sequence<DeclineAndBlockState>
get() = sequenceOf(
aDeclineAndBlockState(),
aDeclineAndBlockState(
reportRoom = true,
reportReason = "Inappropriate content",
),
aDeclineAndBlockState(
blockUser = true,
),
aDeclineAndBlockState(
declineAction = AsyncAction.Loading,
),
aDeclineAndBlockState(
declineAction = AsyncAction.Failure(Exception("Failed to decline")),
),
)
}
fun aDeclineAndBlockState(
reportRoom: Boolean = false,
reportReason: String = "",
blockUser: Boolean = false,
declineAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DeclineAndBlockEvents) -> Unit = {},
) = DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
blockUser = blockUser,
declineAction = declineAction,
eventSink = eventSink,
)

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.invite.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val isDeclining = state.declineAction is AsyncAction.Loading
AsyncActionView(
async = state.declineAction,
onSuccess = { onBackClick() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.screen_decline_and_block_title),
style = ElementTheme.typography.aliasScreenTitle,
)
},
navigationIcon = {
BackButton(onClick = onBackClick)
}
)
},
modifier = modifier
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.padding(end = 8.dp),
headlineContent = {
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title))
},
supportingContent = {
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
},
trailingContent = ListItemContent.Switch(checked = state.blockUser)
)
Spacer(modifier = Modifier.height(24.dp))
ListItem(
modifier = Modifier.padding(end = 8.dp),
headlineContent = {
Text(text = stringResource(CommonStrings.action_report_room))
},
supportingContent = {
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
},
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
)
if (state.reportRoom) {
Spacer(modifier = Modifier.height(24.dp))
TextField(
value = state.reportReason,
onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
minLines = 3,
enabled = !isDeclining,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.heightIn(min = 90.dp),
)
}
Spacer(modifier = Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_decline),
destructive = true,
showProgress = isDeclining,
enabled = !isDeclining && state.canDecline,
onClick = {
focusManager.clearFocus(force = true)
state.eventSink(DeclineAndBlockEvents.Decline)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
}
}
@PreviewsDayNight
@Composable
internal fun DeclineAndBlockViewPreview(
@PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState
) = ElementPreview {
DeclineAndBlockView(
state = state,
onBackClick = {},
)
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs))
}
}

View File

@@ -12,9 +12,9 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.acceptdecline.AcceptDeclineInvitePresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient

View File

@@ -1,135 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
val inviteData = event.invite
if (inviteData == null) {
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
}
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
val inviteData = event.invite
if (inviteData == null) {
declinedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
}
}
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
when (val declinedActionValue = declinedAction.value) {
is ConfirmingDeclineInvite -> {
localCoroutineScope.declineInvite(
inviteData = declinedActionValue.inviteData,
declinedAction = declinedAction,
blockUser = declinedActionValue.blockUser,
)
}
else -> Unit
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
}
return AcceptDeclineInviteState(
acceptAction = acceptedAction.value,
declineAction = declinedAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.acceptInvite(
roomId: RoomId,
acceptedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
acceptedAction.runUpdatingState {
joinRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
)
.onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
seenInvitesStore.markAsUnSeen(roomId)
}
.map { roomId }
}
}
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
suspend {
client.getPendingRoom(inviteData.roomId)?.use {
it.leave().getOrThrow()
}
if (blockUser) {
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
seenInvitesStore.markAsUnSeen(inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction)
}
}

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti"</string>
<string name="screen_decline_and_block_block_user_option_title">"Zablokovat uživatele"</string>
<string name="screen_decline_and_block_report_user_option_description">"Nahlaste tuto místnost svému poskytovateli účtu."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Popište důvod nahlášení…"</string>
<string name="screen_decline_and_block_title">"Odmítnout a zablokovat"</string>
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>

View File

@@ -1,5 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn"</string>
<string name="screen_decline_and_block_block_user_option_title">"Rhwystro defnyddiwr"</string>
<string name="screen_decline_and_block_report_user_option_description">"Adrodd am yr ystafell hon i ddarparwr eich cyfrif."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Disgrifiwch y rheswm dros adrodd…"</string>
<string name="screen_decline_and_block_title">"Gwrthod a rhwystro"</string>
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Gwrthod sgwrs"</string>
<string name="screen_invites_empty_list">"Dim Gwahoddiadau"</string>
<string name="screen_invites_invited_you">"Mae %1$s (%2$s) wedi eich gwahodd"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Iawn, gwrthod a rhwystro"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â\'r ystafell hon? Bydd hyn hefyd yn atal %1$s rhag cysylltu â chi neu eich gwahodd i ystafelloedd."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Gwrthod gwahoddiad a rhwystro"</string>

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen."</string>
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beschreiben Sie den Grund für die Meldung…"</string>
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>

View File

@@ -1,9 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Δε θα δείτε μηνύματα ή προσκλήσεις δωματίου από αυτόν τον χρήστη"</string>
<string name="screen_decline_and_block_block_user_option_title">"Αποκλεισμός χρήστη"</string>
<string name="screen_decline_and_block_report_user_option_description">"Αναφέρετε αυτό το δωμάτιο στον πάροχο του λογαριασμού σας."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Περιγράψτε τον λόγο αναφοράς…"</string>
<string name="screen_decline_and_block_title">"Απόρριψη και αποκλεισμός"</string>
<string name="screen_invites_decline_chat_message">"Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;"</string>
<string name="screen_invites_decline_chat_title">"Απόρριψη πρόσκλησης"</string>
<string name="screen_invites_decline_direct_chat_message">"Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"</string>
<string name="screen_invites_decline_direct_chat_title">"Απόρριψη συνομιλίας"</string>
<string name="screen_invites_empty_list">"Χωρίς προσκλήσεις"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) σέ προσκάλεσε"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ναι, απόρριψη και αποκλεισμός"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Είστε βέβαιοι ότι θέλετε να απορρίψετε την πρόσκληση συμμετοχής σε αυτό το δωμάτιο; Αυτό θα εμποδίσει επίσης το χρήστη %1$s να επικοινωνήσει μαζί σας ή να σας προσκαλέσει σε δωμάτια."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Απόρριψη πρόσκλησης και αποκλεισμός"</string>
<string name="screen_join_room_decline_and_block_button_title">"Απόρριψη και αποκλεισμός"</string>
</resources>

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid"</string>
<string name="screen_decline_and_block_block_user_option_title">"Blokeeri kasutaja"</string>
<string name="screen_decline_and_block_report_user_option_description">"Teata sellest jututoast oma teenusepakkujale."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Kirjelda teatamise põhjust…"</string>
<string name="screen_decline_and_block_title">"Keeldu ja blokeeri"</string>
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>

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