Merge develop into the wildcard mime type branch

Develop gained resolveImageMimeType, which reads the image header so a
share that declares a broad type is encoded as the format it really is.
Resolving the wildcard first would hand that helper a concrete image/jpeg
and make it return early, so a transparent PNG shared as image/* would be
flattened and an animated GIF would be recompressed. The header is now
read from the type the sender declared and the default subtype is applied
to whatever comes back, so the sniffing still runs and an undecodable file
no longer keeps a wildcard.

A non-image shared as image/* is therefore sent as image/jpeg rather than
image/*, which is the whole point of this branch, so the test covering
that case now expects the default subtype.
This commit is contained in:
hayaksi1
2026-08-20 14:26:57 +03:00
418 changed files with 6374 additions and 4183 deletions
-21
View File
@@ -1,21 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "matrix.org"
- tapOn:
id: "login-change_server"
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
- hideKeyboard
- extendedWaitUntil:
visible: "element.io"
timeout: 10000
- tapOn: "element.io"
# Revert to matrix.org
- tapOn:
id: "login-change_server"
- tapOn: "matrix.org"
+1 -2
View File
@@ -3,8 +3,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn
- runFlow: changeServer.yaml
- runFlow: ../assertions/assertLoginDisplayed.yaml
# The account provider field defaults to matrix.org, so continue straight to the MAS login.
- tapOn:
id: "login-continue"
## MAS page
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Change account provider"
visible: "Enter your account provider"
timeout: 10000
+83
View File
@@ -1,3 +1,86 @@
Changes in Element X v26.08.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
**Note: this release in an internal release, it will not be pushed to the stores.**
## What's Changed
### ✨ Features
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
### 🙌 Improvements
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
### 🐛 Bugfixes
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
### 🧱 Build
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
### 📄 Documentation
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
### 🚧 In development 🚧
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
### Dependency upgrades
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
### 💼 Element Pro
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
### Others
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
## New Contributors
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
Changes in Element X v26.08.1
=============================
+1
View File
@@ -259,6 +259,7 @@ dependencies {
if (isEnterpriseBuild) {
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
implementation(project(":enterprise:features:enterprise:shared"))
} else {
implementation(projects.features.enterprise.implFoss)
implementation(projects.appicon.element)
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
@@ -74,17 +74,18 @@ object AppModule {
fun providesBuildMeta(
@ApplicationContext context: Context,
buildType: BuildType,
enterpriseService: EnterpriseService,
isEnterpriseBuild: IsEnterpriseBuild,
): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
val isEnterprise = isEnterpriseBuild()
return BuildMeta(
isDebuggable = BuildConfig.DEBUG,
buildType = buildType,
applicationName = applicationName,
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
isEnterpriseBuild = isEnterprise,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME,
@@ -1,16 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
object PushConfig {
/**
* Note: pusher_app_id cannot exceed 64 chars.
*/
const val PUSHER_APP_ID: String = "im.vector.app.android"
}
@@ -51,7 +51,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
@@ -157,7 +157,7 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -685,7 +685,7 @@ class LoggedInFlowNode(
val currentMapTilerConfig = LocalMapTilerConfig.current
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
}
ElementThemeApp(
@@ -115,7 +115,7 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create(
syncService = matrixClient.syncService,
matrixClient = matrixClient,
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
@AssistedInject
class SyncOrchestrator(
@Assisted private val syncService: SyncService,
@Assisted private val matrixClient: MatrixClient,
@Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
@@ -47,13 +47,15 @@ class SyncOrchestrator(
@AssistedFactory
interface Factory {
fun create(
syncService: SyncService,
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator
}
private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
@@ -94,7 +96,7 @@ class SyncOrchestrator(
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation
appForegroundStateService.isSharingLiveLocation,
)
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine(
@@ -103,6 +105,11 @@ class SyncOrchestrator(
networkMonitor.connectivity,
isAppActiveFlow,
) { syncState, networkState, isAppActive ->
if (matrixClient.isShuttingDown) {
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
return@combine SyncStateAction.NoOp
}
val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -76,6 +76,7 @@ class IntentResolver(
}
private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"تسجيل الخروج والترقية"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"لم يعد %1$s يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"لم يعد خادمك المنزلي يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
</resources>
@@ -2,4 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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>
@@ -12,6 +12,7 @@ import io.element.android.appnav.di.SyncOrchestrator
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
initialHasRingingCall = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = true,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
startSyncRecorder.assertions().isNeverCalled()
}
@Test
fun `when the client is shutting down, the orchestrator does nothing`() = runTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false,
initialIsSyncingNotificationEventValue = false,
initialIsInCallValue = false,
)
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val syncOrchestrator = createSyncOrchestrator(
matrixClient = FakeMatrixClient(syncService = syncService, isShuttingDownResult = { true }),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// These should still not trigger a sync, sync the client is shutting down
networkMonitor.givenNetworkBlocked(false)
appForegroundStateService.updateIsInCallState(true)
appForegroundStateService.isInForeground.value = true
advanceTimeBy(10.seconds)
startSyncRecorder.assertions().isNeverCalled()
}
private fun TestScope.createSyncOrchestrator(
syncService: FakeSyncService = FakeSyncService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
syncService = syncService,
matrixClient = matrixClient,
sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
@@ -11,7 +11,7 @@ package io.element.android.appnav.di
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
return object : SyncOrchestrator.Factory {
override fun create(
syncService: SyncService,
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator {
return SyncOrchestrator(
syncService = syncService,
matrixClient = matrixClient,
sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
assertThat(result).isNull()
}
@Test
fun `resolve intent replayed from the history should return null`() {
val sut = createIntentResolver(
deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
eventId = null,
)
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
@Test
fun `test resolve navigation intent root`() {
val sut = createIntentResolver(
@@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_settings_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_settings_share_data">"مشاركة البيانات الإحصائية"</string>
</resources>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"لن نسجّل أو ننشئ ملفات شخصية لأي معلومات شخصية"</string>
<string name="screen_analytics_prompt_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_prompt_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_prompt_settings">"يمكنك إطفاء هذا الخيار في أي وقت"</string>
<string name="screen_analytics_prompt_third_party_sharing">"لن نشارك بياناتك مع أطراف ثالثة"</string>
<string name="screen_analytics_prompt_title">"ساعدنا على التحسن %1$s"</string>
</resources>
@@ -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_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"مكالمة جارية"</string>
<string name="call_foreground_service_message_android">"اضغط للعودة إلى المكالمة"</string>
<string name="call_foreground_service_title_android">"☎️ الاتصال يتمّ"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"لا يدعم Element Call استخدام أجهزة الصوت عبر Bluetooth في إصدار Android هذا. يرجى اختيار جهاز صوت آخر."</string>
<string name="screen_incoming_call_subtitle_android">"مكالمة Element Call واردة"</string>
</resources>
@@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"برنامه Element Call در این نسخه اندروید از دستگاه‌های صوتی بلوتوث پشتیبانی نمی‌کند. لطفاً دستگاه صوتی دیگری را انتخاب کنید."</string>
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"إنشاء غرفة جديدة"</string>
<string name="screen_create_room_add_people_title">"دعوة أشخاص"</string>
<string name="screen_create_room_error_creating_room">"حصل خطأ أثناء إنشاء الغرفة"</string>
<string name="screen_create_room_error_creating_space">"تعذّر إنشاء المساحة بسبب خطأ غير معروف. حاول مرة أخرى لاحقًا."</string>
<string name="screen_create_room_name_placeholder">"إضافة اسم…"</string>
<string name="screen_create_room_new_room_title">"غرفة جديدة"</string>
<string name="screen_create_room_new_space_title">"مساحة جديدة"</string>
<string name="screen_create_room_private_option_description">"لا يدخل إلا المدعوّون."</string>
<string name="screen_create_room_private_option_title">"خاصة"</string>
<string name="screen_create_room_public_option_description">"بإمكان الجميع العثور على هذه الغرفة.
يمكنك تغيير هذا من إعدادات الغرفة متى شئت."</string>
<string name="screen_create_room_public_option_short_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"يمكن لأي شخص طلب الانضمام، لكن يجب أن يقبل الطلب مسؤول أو مشرف."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"السماح بطلبات الانضمام"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام، لكن يجب على الآخرين طلب الوصول."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"طلب الانضمام"</string>
<string name="screen_create_room_room_access_section_private_option_description">"يمكن للمدعوين فقط الانضمام."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خاصة"</string>
<string name="screen_create_room_room_access_section_public_option_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"قياسي"</string>
<string name="screen_create_room_room_access_section_title">"من يمكنه الوصول"</string>
<string name="screen_create_room_room_address_section_footer">"تحتاج إلى عنوان حتى تظهر الغرفة في الدليل العام."</string>
<string name="screen_create_room_room_address_section_title">"العنوان"</string>
<string name="screen_create_room_room_visibility_section_title">"ظهور الغرفة"</string>
<string name="screen_create_room_space_selection_no_space_description">"(لا توجد مساحة)"</string>
<string name="screen_create_room_space_selection_no_space_option">"عدم الإضافة إلى مساحة"</string>
<string name="screen_create_room_space_selection_no_space_title">"لم يتم اختيار أي مساحة"</string>
<string name="screen_create_room_space_selection_sheet_title">"إضافة إلى مساحة"</string>
<string name="screen_create_room_topic_label">"الموضوع (اختياري)"</string>
<string name="screen_create_room_topic_placeholder">"إضافة وصف…"</string>
</resources>
@@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
<string name="screen_create_room_add_people_title">"Invitar personas"</string>
<string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string>
<string name="screen_create_room_error_creating_space">"No se pudo crear el espacio debido a un error desconocido. Inténtalo de nuevo más tarde."</string>
<string name="screen_create_room_name_placeholder">"Añadir nombre…"</string>
<string name="screen_create_room_new_room_title">"Nueva sala"</string>
<string name="screen_create_room_new_space_title">"Nuevo espacio"</string>
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string>
<string name="screen_create_room_private_option_title">"Privado"</string>
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_public_option_short_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Cualquiera en %1$s puede unirse, pero el resto debe solicitar acceso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Solicitar unirse"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Solo las personas invitadas pueden unirse."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privado"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Cualquiera de %1$s puede unirse."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Estándar"</string>
<string name="screen_create_room_room_access_section_title">"Quién tiene acceso"</string>
<string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string>
<string name="screen_create_room_room_address_section_title">"Dirección"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_space_selection_no_space_description">"(ningún espacio)"</string>
<string name="screen_create_room_space_selection_no_space_option">"No añadir a un espacio"</string>
<string name="screen_create_room_space_selection_no_space_title">"No se ha seleccionado ningún espacio"</string>
<string name="screen_create_room_space_selection_sheet_title">"Añadir a un espacio"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
<string name="screen_create_room_topic_placeholder">"Añadir descripción…"</string>
</resources>
@@ -3,12 +3,34 @@
<string name="screen_create_room_action_create_room">"اتاق جدید"</string>
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
<string name="screen_create_room_error_creating_space">"به دلیل خطای ناشناخته‌ای، فضا ایجاد نشد. بعداً دوباره امتحان کنید."</string>
<string name="screen_create_room_name_placeholder">"افزودن نام…"</string>
<string name="screen_create_room_new_room_title">"اتاق جدید"</string>
<string name="screen_create_room_new_space_title">"فضای جدید"</string>
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</string>
<string name="screen_create_room_private_option_title">"خصوصی"</string>
<string name="screen_create_room_public_option_description">"هرکسی می‌تواند اتاق را بیابد.
می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
<string name="screen_create_room_public_option_short_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"هر کسی می‌تواند درخواست عضویت بدهد، اما یک مدیر یا ناظر باید درخواست را بپذیرد."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"اجازه درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"هر کسی در %1$s می‌تواند عضو شود، اما بقیه باید درخواست دسترسی بدهند."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خصوصی"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"هر کسی که در %1$s باشد می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"استاندارد"</string>
<string name="screen_create_room_room_access_section_title">"چه کسی دسترسی دارد"</string>
<string name="screen_create_room_room_address_section_footer">"برای اینکه بتوانید آن را در فهرست عمومی مشاهده کنید، به یک آدرس نیاز دارید."</string>
<string name="screen_create_room_room_address_section_title">"آدرس"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_space_selection_no_space_description">"(بدون فضا)"</string>
<string name="screen_create_room_space_selection_no_space_option">"به یک فضا اضافه نکنید"</string>
<string name="screen_create_room_space_selection_no_space_title">"فضایی انتخاب نشده است"</string>
<string name="screen_create_room_space_selection_sheet_title">"به فضا اضافه کنید"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_create_room_topic_placeholder">"اضافه کردن توضیحات…"</string>
</resources>
@@ -299,10 +299,10 @@ private fun Content(
}
/**
* Ensure that the string does not contain any new line characters, which can happen when pasting values.
* Ensure that the string does not contain any line separator, which can happen when pasting values.
*/
private fun String.sanitize(): String {
return replace("\n", "")
return filterNot { it == '\n' || it == '\r' }
}
@PreviewsDayNight
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"يرجى تأكيد رغبتك في حذف حسابك. لا يمكن التراجع عن هذا الإجراء."</string>
<string name="screen_deactivate_account_delete_all_messages">"حذف جميع رسائلي"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"تحذير: قد يرى المستخدمون في المستقبل محادثات غير مكتملة."</string>
<string name="screen_deactivate_account_description">"حذف حسابك %1$s، وسيؤدي إلى:"</string>
<string name="screen_deactivate_account_description_bold_part">"إجراء لا رجعة فيه"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s (لن تتمكن من تسجيل الدخول مرة أخرى، ولا يمكن إعادة استخدام معرّفك)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"تعطيل حسابك نهائيًا"</string>
<string name="screen_deactivate_account_list_item_2">"إزالتك من جميع غرف الدردشة."</string>
<string name="screen_deactivate_account_list_item_3">"حذف معلومات حسابك من خادم الهوية لدينا."</string>
<string name="screen_deactivate_account_list_item_4">"ستظل رسائلك مرئية للمستخدمين المسجلين، لكنها لن تكون متاحة للمستخدمين الجدد أو غير المسجلين إذا اخترت حذفها."</string>
<string name="screen_deactivate_account_title">"حذف الحساب"</string>
</resources>
@@ -1,13 +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">"Confirma que quieres desactivar tu cuenta. Esta acción no se puede deshacer."</string>
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirma que quieres eliminar tu cuenta. Esta acción no se puede deshacer."</string>
<string name="screen_deactivate_account_delete_all_messages">"Borrar todos mis mensajes"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Advertencia: Futuros usuarios pueden ver conversaciones incompletas."</string>
<string name="screen_deactivate_account_description">"Desactivar tu cuenta es %1$s:"</string>
<string name="screen_deactivate_account_description">"Eliminar tu cuenta es %1$s:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversible"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s tu cuenta (no podrás volver a iniciar sesión y tu ID no se podrá volver a utilizar)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Inhabilitará permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Te eliminará de todas las salas de chat."</string>
<string name="screen_deactivate_account_list_item_3">"Eliminará la información de tu cuenta de nuestro servidor de identidad."</string>
<string name="screen_deactivate_account_list_item_4">"Tus mensajes seguirán siendo visibles para los usuarios registrados, pero no estarán disponibles para los usuarios nuevos o no registrados si decides eliminarlos."</string>
<string name="screen_deactivate_account_title">"Eliminar cuenta"</string>
</resources>
@@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"لطفاً تأیید کنید که می‌خواهید حساب کاربری خود را حذف کنید. این اقدام قابل بازگشت نیست."</string>
<string name="screen_deactivate_account_delete_all_messages">"حذف همهٔ پیام‌هایم"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"هشدار: کاربران آینده ممکن است مکالمات ناقص را ببینند."</string>
<string name="screen_deactivate_account_description">"حذف حساب کاربری شما %1$s است، این کار باعث می‌شود:"</string>
<string name="screen_deactivate_account_description_bold_part">"بازگشت‌ناپذیر"</string>
<string name="screen_deactivate_account_list_item_1">"حساب کاربری خود را %1$s کنید (شما نمی‌توانید دوباره وارد سیستم شوید و شناسه شما قابل استفاده مجدد نیست)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"از کار انداختن دایمی"</string>
<string name="screen_deactivate_account_list_item_2">"برداشتنتان از همهٔ اتاق‌های گپ."</string>
<string name="screen_deactivate_account_list_item_3">"حذف اطّلاعات حسابتان از کارساز هویت."</string>
<string name="screen_deactivate_account_title">"غیرفعّال‌سازی حساب"</string>
<string name="screen_deactivate_account_list_item_4">"پیام‌های شما همچنان برای کاربران ثبت‌نام‌شده قابل مشاهده خواهند بود، اما اگر تصمیم به حذف آنها بگیرید، برای کاربران جدید یا ثبت‌نام‌نشده در دسترس نخواهند بود."</string>
<string name="screen_deactivate_account_title">"حذف حساب کاربری"</string>
</resources>
@@ -10,9 +10,8 @@ package io.element.android.features.enterprise.api
import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
/**
@@ -21,9 +20,6 @@ import kotlinx.coroutines.flow.Flow
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
*/
interface EnterpriseService {
/** Whether this build is an enterprise one; `false` for the public app. */
val isEnterpriseBuild: Boolean
/**
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
*
@@ -35,23 +31,31 @@ interface EnterpriseService {
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
*
* @param url the URL to rewrite.
* @param homeserver the homeserver the URL belongs to.
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
*/
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String
/**
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
* Returns the list of homeservers the user is allowed to sign in to.
*
* If the list is empty or contains the special value [ANY_ACCOUNT_PROVIDER], the user is allowed to sign in to any homeserver.
*/
fun defaultHomeserverList(): List<String>
fun homeserverAllowList(): List<String>
/**
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
* Whether the user is allowed to sign in to a given homeserver, according to [homeserverAllowList].
*
* @param homeserverUrl the server the user is trying to use.
*/
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
/**
* Whether the given homeserver enforces the use of Element Pro or a derived app.
*
* @param serverName the homeserver to check.
*/
suspend fun isElementProEnforced(serverName: String): Boolean
/**
* Override the brand color.
* @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session.
@@ -94,26 +98,13 @@ interface EnterpriseService {
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
/**
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
*/
fun overriddenElementWellKnown(): ElementWellKnown?
/**
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
* Returns `null` when this build does not read its configuration from an ESS deployment.
*
* @param domain the server whose configuration endpoint is requested.
*/
fun essConfigEndpointUrl(domain: String): String?
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"
}
}
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
return defaultHomeserverList().let {
return homeserverAllowList().let {
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api
/**
* A use case to check if this build of the app is an enterprise build.
*/
fun interface IsEnterpriseBuild {
/**
* Returns `true` if this build of the app is an enterprise build, `false` otherwise.
*/
operator fun invoke(): Boolean
}
@@ -14,6 +14,9 @@ package io.element.android.features.enterprise.api
* [init] must have run before the other members return meaningful answers.
*/
interface SessionEnterpriseService {
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
/** Whether Element Call may be used on this session, which a deployment can turn off. */
suspend fun isElementCallAvailable(): Boolean
@@ -26,7 +29,4 @@ interface SessionEnterpriseService {
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
suspend fun isEncryptionDisabledByHomeserver(): Boolean
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
/**
* A provider of a runtime-fetched MapTiler configuration, which can be used to override the default MapTiler configuration.
*/
fun interface CustomMapTilerConfigProvider {
suspend fun get(): Result<MapTilerConfig?>
}
@@ -1,29 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.wellknown.api
package io.element.android.features.enterprise.api.remoteconfig
data class ElementWellKnown(
val registrationHelperUrl: String?,
val enforceElementPro: Boolean?,
val rageshakeUrl: String?,
val brandColor: String?,
val notificationSound: String?,
val identityProviderAppScheme: String?,
val customRecoveryPassphrase: CustomRecoveryPassphrase?,
val contentScannerUrl: String?,
val forceDisableE2EE: Boolean?,
val mapTilerConfig: MapTilerConfig?,
)
import androidx.compose.runtime.Immutable
/**
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
*/
@Immutable
data class MapTilerConfig(
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
val apiKey: String,
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
import io.element.android.libraries.wellknown.api.ElementWellKnown
/**
* Typealias for the well-known configuration used for enterprise features.
*
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
*/
typealias RemoteEnterpriseConfig = ElementWellKnown
@@ -1,35 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
/**
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
*/
interface RemoteEnterpriseConfigProvider {
/**
* Returns the configuration of a session, served from the cache when possible.
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
*
* @param sessionId the session whose configuration is requested.
*/
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
/**
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
*/
interface Factory {
/**
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
*/
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
sealed interface RemoteEnterpriseConfigResult<out T> {
/**
* Well-known data has been successfully retrieved.
*/
data class Success<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data has been retrieved from the local cache but is outdated and a manual refresh has been launched.
*/
data class Outdated<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data is not found (file does not exist server side, we got a 404). `needsRefresh` indicates whether a refresh must be done
* to replace this value: if it's `true`, the caller must trigger a refresh to try to fetch the data again;
* if it's `false`, the caller can assume that the data is not available and no refresh is needed before returning the data.
*/
data class NotFound(val needsRefresh: Boolean) : RemoteEnterpriseConfigResult<Nothing>
/**
* Any other error.
*/
data class Error(val exception: Exception) : RemoteEnterpriseConfigResult<Nothing>
fun dataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> data
is Error -> null
is NotFound -> null
}
fun upToDateDataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> null
is Error -> null
is NotFound -> null
}
}
@@ -14,20 +14,18 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultEnterpriseService : EnterpriseService {
override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url
override fun homeserverAllowList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementProEnforced(serverName: String): Boolean = false
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@@ -47,8 +45,4 @@ class DefaultEnterpriseService : EnterpriseService {
}
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
override fun overriddenElementWellKnown(): ElementWellKnown? = null
override fun essConfigEndpointUrl(domain: String): String? = null
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.IsEnterpriseBuild
@ContributesBinding(AppScope::class)
class DefaultIsEnterpriseBuild : IsEnterpriseBuild {
override fun invoke(): Boolean = false
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl.remoteconfig
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class)
class DefaultCustomMapTilerConfigProvider : CustomMapTilerConfigProvider {
override suspend fun get(): Result<MapTilerConfig?> = Result.success(null)
}
@@ -1,30 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl.remoteconfig
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
@ContributesBinding(AppScope::class)
class DefaultRemoteEnterpriseConfigProvider : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return WellknownRetrieverResult.NotFound
}
@ContributesBinding(AppScope::class)
class Factory : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider {
return DefaultRemoteEnterpriseConfigProvider()
}
}
}
@@ -9,7 +9,7 @@ package io.element.android.features.enterprise.impl.scanner
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
@@ -22,7 +22,7 @@ class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
@ContributesBinding(AppScope::class)
class Factory : ContentScannerUrlProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider {
override fun create(urlContentFetcher: ClientUrlContentFetcher): ContentScannerUrlProvider {
return DefaultContentScannerUrlProvider()
}
}
@@ -19,15 +19,9 @@ import org.junit.Test
class DefaultEnterpriseServiceTest {
@Test
fun `isEnterpriseBuild is false`() {
fun homeserverWhitelist() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
}
@Test
fun `defaultHomeserverList should return empty list`() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
}
@Test
@@ -12,9 +12,8 @@ import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
@@ -32,9 +30,8 @@ class FakeEnterpriseService(
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
private val tweakMasUrlResult: (String, ClientUrlContentFetcher) -> String = { _, _ -> lambdaError() },
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@@ -43,11 +40,11 @@ class FakeEnterpriseService(
isEnterpriseUserResult(sessionId)
}
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String = simulateLongTask {
tweakMasUrlResult(url, homeserver, urlContentFetcher)
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String = simulateLongTask {
tweakMasUrlResult(url, urlContentFetcher)
}
override fun defaultHomeserverList(): List<String> {
override fun homeserverAllowList(): List<String> {
return defaultHomeserverListResult()
}
@@ -55,6 +52,10 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override suspend fun isElementProEnforced(serverName: String): Boolean = simulateLongTask {
isElementProEnforcedResult(serverName)
}
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
overrideBrandColorResult(sessionId, brandColor)
}
@@ -83,12 +84,4 @@ class FakeEnterpriseService(
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
return getNoisyNotificationChannelIdResult(sessionId)
}
override fun overriddenElementWellKnown(): ElementWellKnown? {
return overrideWellKnownResult()
}
override fun essConfigEndpointUrl(domain: String): String {
return essConfigEndpointUrlResult(domain)
}
}
@@ -1,21 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
class FakeRemoteEnterpriseConfigProvider(
private val getResult: (SessionId) -> WellknownRetrieverResult<RemoteEnterpriseConfig> = { WellknownRetrieverResult.NotFound }
) : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return getResult(sessionId)
}
}
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
class FakeRemoteEnterpriseConfigProviderFactory(
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
) : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
fun aMapTilerConfig(
apiKey: String = "test_api_key",
baseUrl: String? = null,
lightStyleId: String? = null,
darkStyleId: String? = null,
) = MapTilerConfig(
apiKey = apiKey,
baseUrl = baseUrl,
lightStyleId = lightStyleId,
darkStyleId = darkStyleId,
)
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"لا يمكنك التأكيد؟"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"إنشاء مفتاح استرداد جديد"</string>
<string name="screen_identity_confirmation_subtitle">"اختر طريقة التحقق لإعداد المراسلة الآمنة."</string>
<string name="screen_identity_confirmation_title">"تأكيد هويتك الرقمية"</string>
<string name="screen_identity_confirmation_use_another_device">"استخدام جهاز آخر"</string>
<string name="screen_identity_confirmation_use_recovery_key">"استخدام مفتاح الاسترداد"</string>
<string name="screen_identity_confirmed_subtitle">"يمكنك الآن قراءة الرسائل أو إرسالها بأمان، ويمكن لأي شخص تدردش معه أن يثق بهذا الجهاز أيضًا."</string>
<string name="screen_identity_confirmed_title">"تم التحقق من الجهاز"</string>
<string name="screen_identity_use_another_device">"استخدام جهاز آخر"</string>
<string name="screen_identity_waiting_on_other_device">"في انتظار الجهاز الآخر…"</string>
<string name="screen_notification_optin_subtitle">"يمكنك تغيير إعداداتك لاحقاً."</string>
<string name="screen_notification_optin_title">"السماح بالإشعارات وعدم تفويت أي رسالة"</string>
</resources>
@@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"¿No puedes confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crear una nueva clave de recuperación"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica este dispositivo para configurar la mensajería segura."</string>
<string name="screen_identity_confirmation_title">"Confirma que eres tú"</string>
<string name="screen_identity_confirmation_subtitle">"Elige cómo verificar para configurar la mensajería segura."</string>
<string name="screen_identity_confirmation_title">"Confirma tu identidad digital"</string>
<string name="screen_identity_confirmation_use_another_device">"Usar otro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usar clave de recuperación"</string>
<string name="screen_identity_confirmed_subtitle">"Ahora puedes leer o enviar mensajes de forma segura y cualquier persona con la que chatees también puede confiar en este dispositivo."</string>
@@ -269,6 +269,11 @@ class HomeFlowNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() {
backstack.pop()
}
},
)
}
is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"عطّل تحسين البطارية لهذا التطبيق للتأكد من استلام جميع الإشعارات."</string>
<string name="banner_battery_optimization_submit_android">"تعطيل التحسين"</string>
<string name="banner_battery_optimization_title_android">"هل لا تصلك الإشعارات؟"</string>
<string name="banner_new_sound_message">"تم تحديث صوت إشعاراتك، ليصبح أوضح وأسرع وأقل إزعاجًا."</string>
<string name="banner_new_sound_title">"حدّثنا الأصوات لديك"</string>
<string name="banner_set_up_recovery_content">"يتم نسخ دردشاتك احتياطيًا تلقائيًا بتشفير من طرف إلى طرف. لاستعادة هذه النسخة الاحتياطية والاحتفاظ بهويتك الرقمية إذا فقدت الوصول إلى جميع أجهزتك، ستحتاج إلى مفتاح الاسترداد."</string>
<string name="banner_set_up_recovery_submit">"الحصول على مفتاح الاسترداد"</string>
<string name="banner_set_up_recovery_title">"احتفِظ بنسخة احتياطية من دردشاتك"</string>
<string name="confirm_recovery_key_banner_message">"أكّد مفتاح الاسترداد الخاص بك للحفاظ على الوصول إلى تخزين المفاتيح وسجل الرسائل."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"أدخل مفتاح الاسترداد"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"هل نسيت مفتاح الاسترداد؟"</string>
<string name="confirm_recovery_key_banner_title">"تخزين المفاتيح لديك غير متزامن"</string>
<string name="full_screen_intent_banner_message">"للتأكد من عدم تفويت أي مكالمة مهمة، يُرجى تغيير إعداداتك للسماح بإشعارات ملء الشاشة عندما يكون هاتفك مقفلاً."</string>
<string name="full_screen_intent_banner_title">"حسّن تجربة مكالماتك"</string>
<string name="screen_home_tab_chats">"المحادثات"</string>
<string name="screen_home_tab_spaces">"المساحات"</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_migration_message">"هذه العملية تُجرى لمرة واحدة، شكراً على انتظارك."</string>
<string name="screen_migration_title">"إعداد حسابك"</string>
<string name="screen_roomlist_a11y_create_message">"إنشاء محادثة جديد أو غرفة"</string>
<string name="screen_roomlist_clear_filters">"مسح عوامل التصفية"</string>
<string name="screen_roomlist_empty_message">"ابدأ الآن بمراسلة من تشاء."</string>
<string name="screen_roomlist_empty_title">"لا محادثات بعد."</string>
<string name="screen_roomlist_filter_favourites">"المفضلة"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"يمكنك إضافة محادثة إلى المفضلة من إعدادات المحادثة.
يمكنك الآن إلغاء تحديد عوامل التصفية لرؤية محادثاتك الأخرى"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"ليست لديك أي محادثات مفضلة بعد"</string>
<string name="screen_roomlist_filter_invites">"الدعوات"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"ليس لديك أي دعوات قيد الانتظار."</string>
<string name="screen_roomlist_filter_low_priority">"منخفضة الأولوية"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"ليست لديك أي دردشات منخفضة الأولوية بعد"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"يمكنك إلغاء تحديد عوامل التصفية لرؤية محادثاتك الأخرى"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"ليست لديك محادثات لهذا الاختيار"</string>
<string name="screen_roomlist_filter_people">"الأشخاص"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"ليست لديك أي رسائل مباشرة بعد"</string>
<string name="screen_roomlist_filter_rooms">"الغرف"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"لست في أي غرفة بعد"</string>
<string name="screen_roomlist_filter_unreads">"غير المقروءة"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"تهانينا!
ليست لديك أي رسائل غير مقروءة!"</string>
<string name="screen_roomlist_knock_event_sent_description">"تم إرسال طلب الانضمام"</string>
<string name="screen_roomlist_main_space_title">"المحادثات"</string>
<string name="screen_roomlist_mark_as_read">"وضع علامة كمقروء"</string>
<string name="screen_roomlist_mark_as_unread">"وضع علامة كغير مقروء"</string>
<string name="screen_roomlist_tombstoned_room_description">"تمت ترقية هذه الغرفة"</string>
<string name="screen_roomlist_your_spaces">"مساحاتك"</string>
<string name="session_verification_banner_message">"يبدو أنك تستخدم جهازاً جديداً. تحقق باستخدام جهاز آخر ليصبح لك وصول إلى رسائلك المشفرة."</string>
<string name="session_verification_banner_title">"أثبت هويتك"</string>
</resources>
@@ -3,9 +3,11 @@
<string name="banner_battery_optimization_content_android">"Deshabilita la optimización de la batería en esta aplicación para asegurarte de que recibes notificaciones."</string>
<string name="banner_battery_optimization_submit_android">"Deshabilitar la optimización"</string>
<string name="banner_battery_optimization_title_android">"¿No llegan las notificaciones?"</string>
<string name="banner_set_up_recovery_content">"Recupera tu identidad criptográfica y tu historial de mensajes con una clave de recuperación si has perdido todos tus dispositivos actuales."</string>
<string name="banner_set_up_recovery_submit">"Configurar la recuperación"</string>
<string name="banner_set_up_recovery_title">"Configura la recuperación para proteger tu cuenta"</string>
<string name="banner_new_sound_message">"Se ha actualizado tu sonido de notificación: más claro, rápido y menos disruptivo."</string>
<string name="banner_new_sound_title">"Hemos actualizado tus sonidos"</string>
<string name="banner_set_up_recovery_content">"Tus chats se respaldan automáticamente con cifrado de extremo a extremo. Para restaurar esta copia de seguridad y conservar tu identidad digital si pierdes el acceso a todos tus dispositivos, necesitarás tu clave de recuperación."</string>
<string name="banner_set_up_recovery_submit">"Obtener clave de recuperación"</string>
<string name="banner_set_up_recovery_title">"Copia de seguridad de tus chats"</string>
<string name="confirm_recovery_key_banner_message">"Confirma tu clave de recuperación para mantener el acceso a tu almacén de claves y al historial de mensajes."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Introduce tu clave de recuperación"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"¿Olvidaste tu clave de recuperación?"</string>
@@ -13,6 +15,7 @@
<string name="full_screen_intent_banner_message">"Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado."</string>
<string name="full_screen_intent_banner_title">"Mejora tu experiencia de llamada"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_home_tab_spaces">"Espacios"</string>
<string name="screen_invites_decline_chat_message">"¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rechazar la invitación"</string>
<string name="screen_invites_decline_direct_chat_message">"¿Estás seguro de que quieres rechazar este chat privado con %1$s?"</string>
@@ -32,6 +35,7 @@ Por ahora, puedes deseleccionar los filtros para ver tus otros chats"</string>
<string name="screen_roomlist_filter_invites">"Invitaciones"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"No tienes ninguna invitación pendiente."</string>
<string name="screen_roomlist_filter_low_priority">"Prioridad baja"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Aún no tienes ningún chat de prioridad baja"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Puedes deseleccionar filtros para ver tus otros chats."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"No tienes chats para esta selección"</string>
<string name="screen_roomlist_filter_people">"Personas"</string>
@@ -46,6 +50,7 @@ Por ahora, puedes deseleccionar los filtros para ver tus otros chats"</string>
<string name="screen_roomlist_mark_as_read">"Marcar como leído"</string>
<string name="screen_roomlist_mark_as_unread">"Marcar como no leído"</string>
<string name="screen_roomlist_tombstoned_room_description">"Esta sala ha sido renovada"</string>
<string name="screen_roomlist_your_spaces">"Tus espacios"</string>
<string name="session_verification_banner_message">"Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados."</string>
<string name="session_verification_banner_title">"Verifica que eres tú"</string>
</resources>
@@ -3,12 +3,16 @@
<string name="banner_battery_optimization_content_android">"از کار انداختن بهینه‌سازی باتری برای این کاره برای اطمینان از گرفتن همهٔ آگاهی‌ها."</string>
<string name="banner_battery_optimization_submit_android">"از کار انداختن بهینه سازی"</string>
<string name="banner_battery_optimization_title_android">"آگاهی‌ها نمی‌رسند؟"</string>
<string name="banner_set_up_recovery_content">"بازگردانی تاریخچهٔ پیام‌ها و هویت رمزنگاشته‌تان با کلید بازیابی در صورت از دست دادن همهٔ افزاره‌های موجودتان."</string>
<string name="banner_new_sound_message">"پینگ اعلان شما به‌روزرسانی شده است—واضح‌تر، سریع‌تر و کم‌دردسرتر."</string>
<string name="banner_new_sound_title">"ما صداهای شما را تازه کرده‌ایم"</string>
<string name="banner_set_up_recovery_content">"چت‌های شما به طور خودکار با رمزگذاری سراسری پشتیبان‌گیری می‌شوند. برای بازیابی این پشتیبان‌گیری و حفظ هویت دیجیتال خود در زمانی که دسترسی به تمام دستگاه‌هایتان را از دست می‌دهید، به کلید بازیابی خود نیاز خواهید داشت."</string>
<string name="banner_set_up_recovery_submit">"دریافت کلید بازیابی"</string>
<string name="banner_set_up_recovery_title">"برپایی بازیابی"</string>
<string name="banner_set_up_recovery_title">"از چت‌هایتان پشتیبان بگیرید"</string>
<string name="confirm_recovery_key_banner_message">"کلید بازیابی خود را تأیید کنید تا دسترسی به حافظه کلیدها و تاریخچه پیام‌هایتان حفظ شود ."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"ورود کلید بازیابیتان"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"کلید بازیابی خود را فراموش کرده‌اید؟"</string>
<string name="confirm_recovery_key_banner_title">"ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده"</string>
<string name="full_screen_intent_banner_message">"برای اطمینان از اینکه هرگز تماس مهمی را از دست نمی‌دهید، لطفاً تنظیمات خود را تغییر دهید تا وقتی تلفنتان قفل است، اعلان‌ها به صورت تمام صفحه نمایش داده شوند."</string>
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_home_tab_chats">"گپ‌ها"</string>
<string name="screen_home_tab_spaces">"فضاها"</string>
@@ -30,6 +34,7 @@
<string name="screen_roomlist_filter_invites">"دعوت‌ها"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"هیچ دعوت منتظری ندارید."</string>
<string name="screen_roomlist_filter_low_priority">"اولویت کم"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"شما هنوز هیچ چت با اولویت پایین ندارید"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"هیچ گپی برای این گزینش ندارید"</string>
<string name="screen_roomlist_filter_people">"افراد"</string>
@@ -44,6 +49,7 @@
<string name="screen_roomlist_mark_as_read">"علامت‌گذاری به عنوان خوانده شده"</string>
<string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string>
<string name="screen_roomlist_tombstoned_room_description">"این اتاق ارتقا یافته"</string>
<string name="screen_roomlist_your_spaces">"فضاهای شما"</string>
<string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string>
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
</resources>
@@ -11,6 +11,7 @@ package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -36,7 +37,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() },
sessionCoroutineScope = backgroundScope,
@@ -10,13 +10,19 @@ package io.element.android.features.invite.api.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint
fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
fun createNode(
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
callback: Callback,
): Node
interface Callback : Plugin {
fun onDeclineSuccess()
}
}
@@ -17,7 +17,9 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@@ -31,6 +33,7 @@ class DeclineAndBlockNode(
data class Inputs(val inviteData: InviteData) : NodeInputs
private val inviteData = inputs<Inputs>().inviteData
private val callback: DeclineInviteAndBlockEntryPoint.Callback = callback()
private val presenter = presenterFactory.create(inviteData)
@Composable
@@ -39,6 +42,7 @@ class DeclineAndBlockNode(
DeclineAndBlockView(
state = state,
onBackClick = ::navigateUp,
onDeclineSuccess = callback::onDeclineSuccess,
modifier = modifier
)
}
@@ -46,6 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun DeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit,
onDeclineSuccess: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
@@ -53,7 +54,7 @@ fun DeclineAndBlockView(
val isDeclining = state.declineAction is AsyncAction.Loading
AsyncActionView(
async = state.declineAction,
onSuccess = { onBackClick() },
onSuccess = { onDeclineSuccess() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
@@ -149,5 +150,6 @@ internal fun DeclineAndBlockViewPreview(
DeclineAndBlockView(
state = state,
onBackClick = {},
onDeclineSuccess = {},
)
}
@@ -22,8 +22,9 @@ class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs))
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs, callback))
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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>
@@ -1,6 +1,8 @@
<?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>
@@ -10,6 +12,7 @@
<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>
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.invite.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@@ -41,6 +42,20 @@ class DeclineAndBlockViewTest : RobolectricTest() {
}
}
@Test
fun `a successful decline invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
declineAction = AsyncAction.Success(Unit),
eventSink = eventsRecorder,
),
onDeclineSuccess = it,
)
}
}
@Test
fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
@@ -112,11 +127,13 @@ class DeclineAndBlockViewTest : RobolectricTest() {
private fun AndroidComposeUiTest<ComponentActivity>.setDeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onDeclineSuccess: () -> Unit = EnsureNeverCalled(),
) {
setContent {
DeclineAndBlockView(
state = state,
onBackClick = onBackClick,
onDeclineSuccess = onDeclineSuccess,
)
}
}
@@ -11,7 +11,9 @@ package io.element.android.features.invite.impl.declineandblock
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.invite.test.anInviteData
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
@@ -31,12 +33,17 @@ class DefaultDeclineAndBlockEntryPointTest {
)
}
val inviteData = anInviteData()
val callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() = lambdaError()
}
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
inviteData = inviteData
inviteData = inviteData,
callback = callback,
)
assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java)
assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData))
assertThat(result.plugins).contains(callback)
}
}
@@ -19,6 +19,7 @@ class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node {
lambdaError()
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"أنت عضو بالفعل"</string>
<string name="screen_invite_users_already_invited">"مَدعُوّ بالفعل"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"ليست لديك حاليًا أي دردشات مع جهات الاتصال هذه. أكّد دعوتهم إلى هذه الغرفة قبل المتابعة."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"ليست لديك حاليًا أي دردشات مع جهة الاتصال هذه. أكّد دعوتها إلى هذه الغرفة قبل المتابعة."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"هل تريد دعوة جهات اتصال جديدة إلى هذه الغرفة؟"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"هل تريد دعوة جهة اتصال جديدة إلى هذه الغرفة؟"</string>
</resources>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Ya eres miembro"</string>
<string name="screen_invite_users_already_a_member">"Ya es miembro"</string>
<string name="screen_invite_users_already_invited">"Ya estás invitado"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"De momento no tienes ningún chat con estos contactos. Confirma que quieres invitarlos a esta sala antes de continuar."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"De momento no tienes ningún chat con este contacto. Confirma que quieres invitarlo a esta sala antes de continuar."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"¿Invitar contactos nuevos a esta sala?"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"¿Invitar contacto nuevo a esta sala?"</string>
</resources>
@@ -2,4 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"از پیش عضو است"</string>
<string name="screen_invite_users_already_invited">"از پیش دعوت شده"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"شما در حال حاضر هیچ گفت‌وگویی با این مخاطبین ندارید. قبل از ادامه، دعوت آنها را به این اتاق تأیید کنید."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"شما در حال حاضر هیچ گفت‌وگویی با این مخاطب ندارید. قبل از ادامه، دعوت او را به این اتاق تأیید کنید."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"مخاطبین جدید را به این اتاق دعوت کنید؟"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"مخاطب جدید را به این اتاق دعوت می‌کنید؟"</string>
</resources>
@@ -69,6 +69,9 @@ class JoinRoomFlowNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() = navigateUp()
},
)
NavTarget.Root -> rootNode(buildContext)
}
@@ -99,7 +102,7 @@ class JoinRoomFlowNode(
acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState,
onAcceptInviteSuccess = {},
onDeclineInviteSuccess = {},
onDeclineInviteSuccess = { navigateUp() },
modifier = Modifier
)
}
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"لقد حظرك %1$s."</string>
<string name="screen_join_room_ban_message">"تم حظرك"</string>
<string name="screen_join_room_ban_reason">"السبب: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"إلغاء الطلب"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"نعم، ألغِ"</string>
<string name="screen_join_room_cancel_knock_alert_description">"هل أنت متأكد من رغبتك في إلغاء طلبك للانضمام إلى هذه الغرفة؟"</string>
<string name="screen_join_room_cancel_knock_alert_title">"إلغاء طلب الانضمام"</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>
<string name="screen_join_room_fail_message">"فشل الانضمام"</string>
<string name="screen_join_room_fail_reason">"إما أنك تحتاج إلى دعوة للانضمام، أو قد توجد قيود على الوصول."</string>
<string name="screen_join_room_forget_action">"نسيان"</string>
<string name="screen_join_room_invite_required_message">"تحتاج إلى دعوة للانضمام"</string>
<string name="screen_join_room_invited_by">"بدعوة من"</string>
<string name="screen_join_room_join_action">"إنضمام"</string>
<string name="screen_join_room_join_restricted_message">"قد تحتاج إلى دعوة أو إلى أن تكون عضوًا في مساحة لتتمكن من الانضمام."</string>
<string name="screen_join_room_knock_action">"إرسال طلب الانضمام"</string>
<string name="screen_join_room_knock_message_characters_count">"الأحرف المسموح بها: %1$d من %2$d"</string>
<string name="screen_join_room_knock_message_description">"رسالة (اختيارية)"</string>
<string name="screen_join_room_knock_sent_description">"ستتلقى دعوة للانضمام إلى الغرفة إذا تم قبول طلبك."</string>
<string name="screen_join_room_knock_sent_title">"تم إرسال طلب الانضمام"</string>
<string name="screen_join_room_loading_alert_message">"تعذّر عرض معاينة الغرفة. قد يكون ذلك بسبب مشكلات في الشبكة أو الخادم."</string>
<string name="screen_join_room_loading_alert_title">"تعذّر عرض معاينة هذه الغرفة"</string>
<string name="screen_join_room_space_not_supported_description">"لا يدعم %1$s المساحات بعد. يمكنك الوصول إلى المساحات عبر الويب."</string>
<string name="screen_join_room_space_not_supported_title">"المساحات غير مدعومة بعد"</string>
<string name="screen_join_room_subtitle_knock">"انقر على الزر أدناه وسيتم إشعار مسؤول الغرفة. ستتمكن من الانضمام إلى المحادثة بعد الموافقة."</string>
<string name="screen_join_room_subtitle_no_preview">"يجب أن تكون عضوًا في هذه الغرفة لعرض سجل الرسائل."</string>
<string name="screen_join_room_title_knock">"هل تريد الانضمام إلى هذه الغرفة؟"</string>
<string name="screen_join_room_title_no_preview">"المعاينة غير متاحة"</string>
</resources>
@@ -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_join_room_ban_by_message">"Has sido baneado por %1$s."</string>
<string name="screen_join_room_ban_message">"Has sido baneado."</string>
<string name="screen_join_room_ban_by_message">"Has sido vetado por %1$s."</string>
<string name="screen_join_room_ban_message">"Has sido vetado"</string>
<string name="screen_join_room_ban_reason">"Motivo: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar solicitud"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sí, cancelar"</string>
@@ -15,9 +15,11 @@
<string name="screen_join_room_fail_reason">"O es necesario estar invitado para unirse o puede haber restricciones de acceso."</string>
<string name="screen_join_room_forget_action">"Olvidar"</string>
<string name="screen_join_room_invite_required_message">"Necesitas una invitación para unirte"</string>
<string name="screen_join_room_invited_by">"Te invitó"</string>
<string name="screen_join_room_join_action">"Unirse"</string>
<string name="screen_join_room_join_restricted_message">"Es posible que necesites ser invitado o ser miembro de un espacio para poder unirte."</string>
<string name="screen_join_room_knock_action">"Enviar solicitud de unión"</string>
<string name="screen_join_room_knock_message_characters_count">"%1$d de %2$d caracteres permitidos"</string>
<string name="screen_join_room_knock_message_description">"Mensaje (opcional)"</string>
<string name="screen_join_room_knock_sent_description">"Recibirás una invitación para unirte a la sala si se acepta tu solicitud."</string>
<string name="screen_join_room_knock_sent_title">"Solicitud de unión enviada"</string>
@@ -1,23 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"به دست %1$s از این اتاق محروم شدید."</string>
<string name="screen_join_room_ban_message">"از این اتاق محروم شدید"</string>
<string name="screen_join_room_ban_by_message">"شما توسط %1$s مسدود شده‌اید."</string>
<string name="screen_join_room_ban_message">"شما محروم شدید"</string>
<string name="screen_join_room_ban_reason">"دلیل: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"لغو درخواست"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"بله. لغو"</string>
<string name="screen_join_room_cancel_knock_alert_description">"آیا مطمئن هستید که می‌خواهید درخواست عضویت خود در این اتاق را لغو کنید؟"</string>
<string name="screen_join_room_cancel_knock_alert_title">"لغو درخواست پیوستن"</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>
<string name="screen_join_room_fail_message">"پیوستن شکست خورد"</string>
<string name="screen_join_room_forget_action">"فراموشی این اتاق"</string>
<string name="screen_join_room_invite_required_message">"برای پیوستن به این اتاق نیاز به دعوت دارید"</string>
<string name="screen_join_room_fail_reason">"یا باید دعوتنامه داشته باشید یا ممکن است محدودیت‌هایی برای دسترسی وجود داشته باشد."</string>
<string name="screen_join_room_forget_action">"فراموش کردن"</string>
<string name="screen_join_room_invite_required_message">"برای عضویت نیاز به دعوتنامه دارید"</string>
<string name="screen_join_room_invited_by">"دعوت شده از سوی"</string>
<string name="screen_join_room_join_action">"پیوستن"</string>
<string name="screen_join_room_join_restricted_message">"برای پیوستن به فضا باید دعوت شده باشید."</string>
<string name="screen_join_room_knock_action">"در زدن برای پیوستن"</string>
<string name="screen_join_room_knock_message_characters_count">"کاراکترهای مجاز %1$d از %2$d"</string>
<string name="screen_join_room_knock_message_description">"پیام (اختیاری)"</string>
<string name="screen_join_room_knock_sent_description">"در صورت پذیرش درخواست، دعوتنامه‌ای برای ورود به اتاق دریافت خواهید کرد."</string>
<string name="screen_join_room_knock_sent_title">"درخواست پیوستن فرستاده شد"</string>
<string name="screen_join_room_loading_alert_message">"ما نتوانستیم پیش‌نمایش اتاق را نمایش دهیم. این ممکن است به دلیل مشکلات شبکه یا سرور باشد."</string>
<string name="screen_join_room_loading_alert_title">"ما نتوانستیم پیش‌نمایش این اتاق را نمایش دهیم"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s هنوز از فضاها پشتیبانی نمی‌کند. می‌توانید روی وب به فضاها دسترسی داشته باشید."</string>
<string name="screen_join_room_space_not_supported_title">"فضاها هنوز پشتیبانی نمی‌شوند"</string>
<string name="screen_join_room_subtitle_knock">"زدن روی این دکمه برای آگاه شدن مدیر اتاق. پس از تأیید می‌توانید به گفت‌وگو بپیوندید."</string>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"نعم، قبول الكل"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"هل أنت متأكد من رغبتك في قبول جميع طلبات الانضمام؟"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"قبول جميع الطلبات"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"قبول الكل"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"تعذّر قبول جميع الطلبات. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"فشل قبول جميع الطلبات"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"جارٍ قبول جميع طلبات الانضمام"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"تعذّر قبول هذا الطلب. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"فشل قبول الطلب"</string>
<string name="screen_knock_requests_list_accept_loading_title">"جارٍ قبول طلب الانضمام"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"نعم، ارفض واحظر"</string>
<string name="screen_knock_requests_list_ban_alert_description">"هل أنت متأكد من رغبتك في رفض %1$s وحظره؟ لن يتمكن هذا المستخدم من طلب الوصول للانضمام إلى هذه الغرفة مرة أخرى."</string>
<string name="screen_knock_requests_list_ban_alert_title">"رفض الوصول والحظر"</string>
<string name="screen_knock_requests_list_ban_loading_title">"جارٍ رفض الوصول وحظره"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"نعم، ارفض"</string>
<string name="screen_knock_requests_list_decline_alert_description">"هل أنت متأكد من رغبتك في رفض طلب %1$s للانضمام إلى هذه الغرفة؟"</string>
<string name="screen_knock_requests_list_decline_alert_title">"رفض الوصول"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"رفض وحظر"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"تعذّر رفض هذا الطلب. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"فشل رفض الطلب"</string>
<string name="screen_knock_requests_list_decline_loading_title">"جارٍ رفض طلب الانضمام"</string>
<string name="screen_knock_requests_list_empty_state_description">"عندما يطلب أحدهم الانضمام إلى الغرفة، ستتمكن من رؤية طلبه هنا."</string>
<string name="screen_knock_requests_list_empty_state_title">"لا توجد طلبات انضمام معلّقة"</string>
<string name="screen_knock_requests_list_initial_loading_title">"جارٍ تحميل طلبات الانضمام…"</string>
<string name="screen_knock_requests_list_title">"طلبات الانضمام"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="zero">"%1$s يريد الانضمام إلى هذه الغرفة"</item>
<item quantity="one">"%1$s +%2$d آخر يريدان الانضمام إلى هذه الغرفة"</item>
<item quantity="two">"%1$s +%2$d آخران يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="few">"%1$s +%2$d آخرون يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="many">"%1$s +%2$d آخرين يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="other">"%1$s +%2$d آخرين يريدون الانضمام إلى هذه الغرفة"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"عرض الكل"</string>
<string name="screen_room_single_knock_request_accept_button_title">"قبول"</string>
<string name="screen_room_single_knock_request_title">"%1$s يريد الانضمام إلى هذه الغرفة"</string>
<string name="screen_room_single_knock_request_view_button_title">"عرض"</string>
</resources>
@@ -1,21 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"بله. پذیرش همه"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"آیا مطمئن هستید که می‌خواهید همه درخواست‌های عضویت را بپذیرید؟"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"پذیرش همهٔ درخواست‌ها"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"پذیرش همه"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"ما نتوانستیم همه درخواست‌ها را بپذیریم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"شکست در پذیرش همهٔ درخواست‌ها"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"پذیرفتن همهٔ درخواست‌های پیوستن"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"ما نتوانستیم این درخواست را بپذیریم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"شکست در پذیرش درخواست"</string>
<string name="screen_knock_requests_list_accept_loading_title">"پذیرفتن درخواست پیوستن"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"بله. رد و انسداد"</string>
<string name="screen_knock_requests_list_ban_alert_description">"آیا مطمئن هستید که می‌خواهید %1$s را رد و مسدود کنید؟ این کاربر دیگر نمی‌تواند درخواست دسترسی برای پیوستن به این اتاق را بدهد."</string>
<string name="screen_knock_requests_list_ban_alert_title">"رد و تحریم دسترسی"</string>
<string name="screen_knock_requests_list_ban_loading_title">"رد کردن و تحریم دسترسی"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"بله. رد شود"</string>
<string name="screen_knock_requests_list_decline_alert_description">"آیا مطمئن هستید که می‌خواهید درخواست %1$s برای پیوستن به این اتاق را رد کنید؟"</string>
<string name="screen_knock_requests_list_decline_alert_title">"رد کردن دسترسی"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"رد و تحریم"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"ما نمی‌توانستیم این درخواست را رد کنیم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"شکتس در رد درخواست"</string>
<string name="screen_knock_requests_list_decline_loading_title">"در کردن درخواست پیوستن"</string>
<string name="screen_knock_requests_list_empty_state_description">"وقتی کسی درخواست عضویت در اتاق را می‌دهد، می‌توانید درخواست او را اینجا ببینید."</string>
<string name="screen_knock_requests_list_empty_state_title">"درخواست عضویت در حال بررسی نیست"</string>
<string name="screen_knock_requests_list_initial_loading_title">"در حال بارگیری درخواست‌های عضویت…"</string>
<string name="screen_knock_requests_list_title">"درخواست‌های پیوستن"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d افراد دیگری هم می‌خواهند به این اتاق بپیوندند"</item>
<item quantity="other">"%1$s +%2$d دیگران می‌خواهند به این اتاق بپیوندند"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"دیدن همه"</string>
<string name="screen_room_single_knock_request_accept_button_title">"پذیرش"</string>
<string name="screen_room_single_knock_request_title">"%1$s می‌خواهد به این اتاق بپیوندد"</string>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"هل أنت متأكد من رغبتك في مغادرة هذه المحادثة؟ هذه المحادثة ليست عامة ولن تتمكن من الانضمام إليها مرة أخرى دون دعوة."</string>
<string name="leave_room_alert_empty_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟ أنت الشخص الوحيد هنا. وإذا ما غادرت فلن يعود أحد قادراً على الدخول في المستقبل، بما فيهم أنت."</string>
<string name="leave_room_alert_private_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟ هذه الغرفة ليست عامة ولن تعود قادراً على الدخول مجدداً بلا دعوة"</string>
<string name="leave_room_alert_select_new_owner_action">"اختيار المالكين"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"أنت المالك الوحيد لهذه الغرفة. يجب أن تنقل الملكية إلى شخص آخر قبل مغادرة الغرفة."</string>
<string name="leave_room_alert_select_new_owner_title">"نقل الملكية"</string>
<string name="leave_room_alert_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟"</string>
</resources>
@@ -3,5 +3,8 @@
<string name="leave_conversation_alert_subtitle">"¿Estás seguro de que quieres salir de esta conversación? Esta conversación no es pública y no podrás volver a unirte sin una invitación."</string>
<string name="leave_room_alert_empty_subtitle">"¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú."</string>
<string name="leave_room_alert_private_subtitle">"¿Seguro que quieres salir de esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación."</string>
<string name="leave_room_alert_select_new_owner_action">"Elegir propietarios"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"Eres el único propietario de esta sala. Debes transferir la propiedad a otra persona antes de salir de la sala."</string>
<string name="leave_room_alert_select_new_owner_title">"Transferir la propiedad"</string>
<string name="leave_room_alert_subtitle">"¿Seguro que quieres salir de la sala?"</string>
</resources>
@@ -4,6 +4,7 @@
<string name="leave_room_alert_empty_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ تنها فرد این‌جا هستید. در صورت ترک، هیچ‌کسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد."</string>
<string name="leave_room_alert_private_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید."</string>
<string name="leave_room_alert_select_new_owner_action">"گزینش مالکان"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"شما تنها مالک این اتاق هستید. قبل از ترک اتاق، باید مالکیت را به شخص دیگری منتقل کنید."</string>
<string name="leave_room_alert_select_new_owner_title">"انتقال مالکیت"</string>
<string name="leave_room_alert_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟"</string>
</resources>
@@ -60,7 +60,13 @@ class LeaveRoomPresenter(
roomId: RoomId,
leaveAction: MutableState<AsyncAction<Unit>>,
) = launch(dispatchers.io) {
client.getRoom(roomId)?.use { room ->
val room = client.getRoom(roomId)
if (room == null) {
Timber.e("Unable to find room $roomId to leave it")
leaveAction.value = AsyncAction.Failure(RoomNotFoundException(roomId))
return@launch
}
room.use {
val roomInfo = room.roomInfoFlow.first()
leaveAction.value = when {
roomInfo.isDm -> Confirmation.Dm(roomId)
@@ -78,7 +84,8 @@ class LeaveRoomPresenter(
leaveAction: MutableState<AsyncAction<Unit>>,
) = launch(dispatchers.io) {
leaveAction.runCatchingUpdatingState {
client.getRoom(roomId)!!.use { room ->
val room = client.getRoom(roomId) ?: throw RoomNotFoundException(roomId)
room.use {
room
.leave()
.onSuccess { notificationConversationService.onLeftRoom(client.sessionId, roomId) }
@@ -100,3 +107,5 @@ class LeaveRoomPresenter(
}
}
}
internal class RoomNotFoundException(roomId: RoomId) : IllegalStateException("Room $roomId not found")
@@ -175,6 +175,32 @@ class LeaveBaseRoomPresenterTest {
}
}
@Test
fun `present - show error if the room to confirm leaving is not found`() = runTest {
val presenter = createLeaveRoomPresenter(client = FakeMatrixClient())
presenter.stateFlow().test {
val initialState = awaitItem()
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
val errorState = awaitItem()
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - show error if the room to leave is not found`() = runTest {
val presenter = createLeaveRoomPresenter(client = FakeMatrixClient())
presenter.stateFlow().test {
val initialState = awaitItem()
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
val progressState = awaitItem()
assertThat(progressState.leaveAction).isEqualTo(AsyncAction.Loading)
val errorState = awaitItem()
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - reset state after error`() = runTest {
val presenter = createLeaveRoomPresenter(
@@ -39,6 +39,10 @@ class LinkNewDesktopHandler(
LinkDesktopStep.Uninitialized
)
/**
* A [StateFlow] that emits the current step of the link desktop process.
* It starts with [LinkDesktopStep.Uninitialized] and will emit other steps as they occur.
*/
val stepFlow: StateFlow<LinkDesktopStep>
get() = linkDesktopStepFlow.asStateFlow()
@@ -53,15 +57,31 @@ class LinkNewDesktopHandler(
timerJob = null
}
fun createNewHandler() {
resetJobs()
handler = matrixClient.createLinkDesktopHandler().getOrNull()
/**
* Starts (or restarts) a timer that will emit an error in [stepFlow] after 2 minutes.
*/
fun startTimer() {
timerJob?.cancel()
timerJob = sessionScope.launch {
delay(2.minutes)
linkDesktopStepFlow.emit(LinkDesktopStep.Error(ErrorType.Expired("Scanning QrCode took too long.")))
}
}
/**
* Creates a new [LinkDesktopHandler] and cancels any existing job.
* This should be called when the user wants to retry scanning a QR code.
*/
fun createNewHandler() {
currentJob?.cancel()
currentJob = null
handler = matrixClient.createLinkDesktopHandler().getOrNull()
}
/**
* Resets the state of the handler and cancels any existing job.
* This should be called when the user wants to start over.
*/
fun reset() {
resetJobs()
sessionScope.launch {
@@ -69,6 +89,12 @@ class LinkNewDesktopHandler(
}
}
/**
* Handles the scanned QR code data.
* This should be called when the user scans a QR code.
*
* @param data The scanned QR code data as a byte array.
*/
fun onScannedCode(data: ByteArray) {
resetJobs()
val currentHandler = handler
@@ -263,6 +263,8 @@ class LinkNewDeviceFlowNode(
}
LinkDeviceType.Desktop -> {
linkNewDesktopHandler.reset()
// Ensure unlock state does not last longer than the timeout for scanning the QR code, so start the timer after unlock
linkNewDesktopHandler.startTimer()
backstack.push(NavTarget.DesktopNotice)
}
}
@@ -278,10 +280,14 @@ class LinkNewDeviceFlowNode(
NavTarget.DesktopNotice -> {
val callback = object : DesktopNoticeNode.Callback {
override fun navigateBack() {
// Stop the timer when the user leaves the screen (user will have to authenticate again)
linkNewDesktopHandler.reset()
backstack.pop()
}
override fun navigateToQrCodeScanner() {
// Start again the timer when the user enters the next screen (they clicked on "Ready to scan")
linkNewDesktopHandler.startTimer()
backstack.push(NavTarget.DesktopScanQrCode)
}
}
@@ -290,6 +296,8 @@ class LinkNewDeviceFlowNode(
NavTarget.DesktopScanQrCode -> {
val callback = object : ScanQrCodeNode.Callback {
override fun cancel() {
// start again the timer when the user goes back to the DesktopNoticeNode
linkNewDesktopHandler.startTimer()
backstack.pop()
}
}
@@ -40,9 +40,19 @@ class LinkNewMobileHandler(
LinkMobileStep.Uninitialized
)
/**
* A [StateFlow] that emits the current step of the link mobile process.
* It starts with [LinkMobileStep.Uninitialized] and will emit other steps as they occur.
*/
val stepFlow: StateFlow<LinkMobileStep>
get() = linkMobileStepFlow.asStateFlow()
/**
* Creates a new [LinkMobileHandler] and starts the linking process.
* If [forRotating] is true, it indicates that this is a rotation of the QR code.
*
* @param forRotating Whether this is a rotation of the QR code. Defaults to false.
*/
fun createAndStartNewHandler(forRotating: Boolean = false) {
Timber.tag(loggerTag.value).d("createAndStartNewHandler()")
currentJob?.cancel()
@@ -62,6 +72,10 @@ class LinkNewMobileHandler(
}
}
/**
* Resets the handler and cancels any ongoing job.
* This will also emit [LinkMobileStep.Uninitialized] to the [stepFlow].
*/
fun reset() {
currentJob?.cancel()
currentJob = null
@@ -70,10 +84,18 @@ class LinkNewMobileHandler(
}
}
/**
* Rotates the QR code by creating a new handler and starting the linking process.
* This is typically called when a timeout occurs for the current QR code.
*/
fun rotateQrCode() {
createAndStartNewHandler(forRotating = true)
}
/**
* Handles the case where there are too many QR code rotations.
* This will reset the handler and emit an error to the [stepFlow].
*/
fun onTooManyRotation() {
reset()
sessionScope.launch {
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"امسح رمز QR ضوئيًا"</string>
<string name="screen_link_new_device_desktop_step1">"افتح %1$s على كمبيوتر محمول أو مكتبي"</string>
<string name="screen_link_new_device_desktop_step3">"امسح رمز QR ضوئيًا باستخدام هذا الجهاز"</string>
<string name="screen_link_new_device_desktop_submit">"جاهز للمسح الضوئي"</string>
<string name="screen_link_new_device_desktop_title">"افتح %1$s على كمبيوتر مكتبي للحصول على رمز QR"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"الأرقام غير متطابقة"</string>
<string name="screen_link_new_device_enter_number_notice">"أدخل الرمز المكوّن من رقمين"</string>
<string name="screen_link_new_device_enter_number_subtitle">"سيؤكد هذا أن الاتصال بجهازك الآخر آمن."</string>
<string name="screen_link_new_device_enter_number_title">"أدخل الرقم المعروض على جهازك الآخر"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"لا يدعم موفّر حسابك %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s غير مدعوم"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"لا يدعم موفّر حسابك تسجيل الدخول إلى جهاز جديد باستخدام رمز QR."</string>
<string name="screen_link_new_device_error_not_supported_title">"رمز QR غير مدعوم"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"تم إلغاء تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"تم إلغاء طلب تسجيل الدخول"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"انتهت صلاحية تسجيل الدخول. يرجى المحاولة مرة أخرى."</string>
<string name="screen_link_new_device_error_request_timeout_title">"لم يكتمل تسجيل الدخول في الوقت المحدد"</string>
<string name="screen_link_new_device_mobile_step1">"افتح %1$s على الجهاز الآخر"</string>
<string name="screen_link_new_device_mobile_step2">"اختر %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«تسجيل الدخول باستخدام رمز QR»"</string>
<string name="screen_link_new_device_mobile_step3">"امسح رمز QR المعروض هنا باستخدام الجهاز الآخر"</string>
<string name="screen_link_new_device_mobile_title">"افتح %1$s على الجهاز الآخر"</string>
<string name="screen_link_new_device_root_desktop_computer">"كمبيوتر مكتبي"</string>
<string name="screen_link_new_device_root_loading_qr_code">"جارٍ تحميل رمز QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"جهاز محمول"</string>
<string name="screen_link_new_device_root_title">"ما نوع الجهاز الذي تريد ربطه؟"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"يرجى المحاولة مرة أخرى والتأكد من إدخال الرمز المكوّن من رقمين بشكل صحيح. إذا ظلت الأرقام غير متطابقة، فتواصل مع موفّر حسابك."</string>
<string name="screen_link_new_device_wrong_number_title">"الأرقام غير متطابقة"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"تعذّر إنشاء اتصال آمن بالجهاز الجديد. لا تزال أجهزتك الحالية آمنة ولا داعي للقلق بشأنها."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"ماذا الآن؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"حاول تسجيل الدخول مجددًا باستخدام رمز QR إذا كانت هذه مشكلة في الشبكة."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"إذا واجهت المشكلة نفسها، جرّب شبكة Wi-Fi مختلفة أو استخدم بيانات الهاتف المحمول بدلًا من Wi-Fi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"إذا لم ينجح ذلك، سجّل الدخول يدويًا"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"الاتصال غير آمن"</string>
<string name="screen_qr_code_login_device_code_subtitle">"سيُطلب منك إدخال الرقمين الظاهرين على هذا الجهاز."</string>
<string name="screen_qr_code_login_device_code_title">"أدخل الرقم أدناه على جهازك الآخر"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"تم إلغاء تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_cancelled_title">"تم إلغاء طلب تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"تم رفض تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_declined_title">"تم رفض تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لست بحاجة إلى فعل أي شيء آخر."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"جهازك الآخر مسجّل الدخول بالفعل"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"انتهت صلاحية تسجيل الدخول. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_error_expired_title">"لم يكتمل تسجيل الدخول في الوقت المحدد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"جهازك الآخر لا يدعم تسجيل الدخول إلى %s باستخدام رمز QR.
جرّب تسجيل الدخول يدويًا، أو امسح رمز QR ضوئيًا باستخدام جهاز آخر."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"رمز QR غير مدعوم"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"لا يدعم موفّر حسابك %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s غير مدعوم"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"استخدم رمز QR المعروض على الجهاز الآخر."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"حاول مرة أخرى"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"رمز QR غير صحيح"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"تحتاج إلى منح %1$s إذنًا لاستخدام كاميرا جهازك للمتابعة."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"اسمح بالوصول إلى الكاميرا لمسح رمز QR ضوئيًا"</string>
<string name="screen_qr_code_login_unknown_error_description">"حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_verify_code_loading">"في انتظار جهازك الآخر"</string>
</resources>
@@ -1,16 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"Escanea el código QR"</string>
<string name="screen_link_new_device_desktop_step1">"Abre %1$s en un ordenador"</string>
<string name="screen_link_new_device_desktop_step3">"Escanea el código QR con este dispositivo"</string>
<string name="screen_link_new_device_desktop_submit">"Listo para escanear"</string>
<string name="screen_link_new_device_desktop_title">"Abre %1$s en un ordenador para obtener el código QR"</string>
<string name="screen_link_new_device_desktop_warning">"Solo escanea un código QR después de haber seguido los pasos anteriores en tu propio dispositivo. Nunca escanees un código que te haya enviado otra persona."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Los números no coinciden"</string>
<string name="screen_link_new_device_enter_number_notice">"Introduce el código de 2 dígitos"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Esto verificará que la conexión con tu otro dispositivo sea segura."</string>
<string name="screen_link_new_device_enter_number_title">"Introduce el número que aparece en tu otro dispositivo"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Tu proveedor de cuentas no es compatible con %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s no admitido"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"El proveedor de tu cuenta no admite el inicio de sesión en un nuevo dispositivo con un código QR."</string>
<string name="screen_link_new_device_error_not_supported_title">"Código QR no admitido"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"El inicio de sesión se canceló en el otro dispositivo."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"Solicitud de inicio de sesión cancelada"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"El inicio de sesión ha caducado. Inténtalo de nuevo."</string>
<string name="screen_link_new_device_error_request_timeout_title">"El inicio de sesión no se completó a tiempo"</string>
<string name="screen_link_new_device_mobile_step1">"Abre %1$s en el otro dispositivo"</string>
<string name="screen_link_new_device_mobile_step2">"Selecciona %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«Iniciar sesión con un código QR»"</string>
<string name="screen_link_new_device_mobile_step3">"Escanea el código QR que se muestra aquí con el otro dispositivo"</string>
<string name="screen_link_new_device_mobile_title">"Abre %1$s en el otro dispositivo"</string>
<string name="screen_link_new_device_root_desktop_computer">"Ordenador"</string>
<string name="screen_link_new_device_root_loading_qr_code">"Cargando código QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"Dispositivo móvil"</string>
<string name="screen_link_new_device_root_title">"¿Qué tipo de dispositivo quieres vincular?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"Inténtalo de nuevo y asegúrate de haber introducido correctamente el código de 2 dígitos. Si los números siguen sin coincidir, ponte en contacto con el proveedor de tu cuenta."</string>
<string name="screen_link_new_device_wrong_number_title">"Los números no coinciden"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"No se pudo establecer una conexión segura con el nuevo dispositivo. Tus dispositivos actuales siguen siendo seguros y no tienes que preocuparte por ellos."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"¿Y ahora qué?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Intenta iniciar sesión de nuevo con un código QR en caso de que se trate de un problema de red"</string>
@@ -23,6 +41,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Solicitud de inicio de sesión cancelada"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"El inicio de sesión se rechazó en el otro dispositivo."</string>
<string name="screen_qr_code_login_error_declined_title">"Inicio de sesión rechazado"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"No tienes que hacer nada más."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Tu otro dispositivo ya está conectado"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"El inicio de sesión ha caducado. Inténtalo de nuevo."</string>
<string name="screen_qr_code_login_error_expired_title">"El inicio de sesión no se completó a tiempo"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Tu otro dispositivo no admite el inicio de sesión en %s con un código QR.
@@ -1,16 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"پویش کد پاس"</string>
<string name="screen_link_new_device_desktop_step1">"%1$s را روی لپ‌تاپ یا رایانه رومیزی باز کنید."</string>
<string name="screen_link_new_device_desktop_step3">"پویش کد پاس با این افزاره"</string>
<string name="screen_link_new_device_desktop_submit">"آمادهٔ پویش"</string>
<string name="screen_link_new_device_desktop_title">"برای دریافت کد QR، %1$s را روی رایانه رومیزی باز کنید."</string>
<string name="screen_link_new_device_desktop_warning">"فقط پس از انجام مراحل بالا در دستگاه خود، کد QR را اسکن کنید. هرگز کدی را که توسط شخص دیگری ارسال شده است، اسکن نکنید."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"اعداد مطابقت ندارند"</string>
<string name="screen_link_new_device_enter_number_notice">"کد دو رقمی را وارد کنید"</string>
<string name="screen_link_new_device_enter_number_subtitle">"این کار تأیید می‌کند که اتصال به دستگاه دیگر شما ایمن است."</string>
<string name="screen_link_new_device_enter_number_title">"عدد نمایش داده شده در دستگاه دیگر خود را وارد کنید"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"فراهم کنندهٔ حسابتان از %1$s پشتیبانی نمی‌کند."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s پشتیبانی نمی‌شود"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"ارائه دهنده حساب شما از ورود به دستگاه جدید با کد QR پشتیبانی نمی‌کند."</string>
<string name="screen_link_new_device_error_not_supported_title">"کد پاس پشتیبانی نمی‌شود"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"ورود روی افزارهٔ دیگر لغو شد."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"درخواست ورد لغو شد"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"ورود منقضی شد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_link_new_device_error_request_timeout_title">"ورود در زمان معیّن کامل نشد"</string>
<string name="screen_link_new_device_mobile_step1">"%1$s را در دستگاه دیگر باز کنید"</string>
<string name="screen_link_new_device_mobile_step2">"گزینش %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«ورود با کد QR»"</string>
<string name="screen_link_new_device_mobile_step3">"کد QR نشان داده شده در اینجا را با دستگاه دیگر اسکن کنید"</string>
<string name="screen_link_new_device_mobile_title">"%1$s را در دستگاه دیگر باز کنید"</string>
<string name="screen_link_new_device_root_desktop_computer">"کامپیوتر رومیزی"</string>
<string name="screen_link_new_device_root_loading_qr_code">"در حال بارگذاری کد QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"دستگاه همراه"</string>
<string name="screen_link_new_device_root_title">"چه نوع دستگاهی را می‌خواهید لینک کنید؟"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"لطفاً دوباره امتحان کنید و مطمئن شوید که کد دو رقمی را به درستی وارد کرده‌اید. اگر اعداد هنوز مطابقت ندارند، با ارائه دهنده حساب خود تماس بگیرید."</string>
<string name="screen_link_new_device_wrong_number_title">"اعداد مطابقت ندارند"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"اکنون چه؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"آزمودن ورود دوباره با کد پاس در صورت مشکل شبکه"</string>
@@ -23,6 +41,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"درخواست ورد لغو شد"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"ورود به دست افزارهٔ دیگر رد شد."</string>
<string name="screen_qr_code_login_error_declined_title">"ورود رد شد"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لازم نیست کار دیگری انجام دهید."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"دستگاه دیگر شما قبلاً وارد سیستم شده است"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"ورود منقضی شد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_qr_code_login_error_expired_title">"ورود در زمان معیّن کامل نشد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"افزارهٔ دیگرتان از ورود به %s با کد پاس پشتیبانی نمی‌کند.
@@ -5,6 +5,7 @@
<string name="screen_link_new_device_desktop_step3">"Skannaa QR-koodi tällä laitteella"</string>
<string name="screen_link_new_device_desktop_submit">"Valmis skannaamaan"</string>
<string name="screen_link_new_device_desktop_title">"Avaa %1$s pöytätietokoneella saadaksesi QR-koodin"</string>
<string name="screen_link_new_device_desktop_warning">"Skannaa QR-koodi vasta, kun olet noudattanut yllä olevia ohjeita omalla laitteellasi. Älä koskaan skannaa toisen henkilön lähettämää koodia."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numerot eivät täsmää"</string>
<string name="screen_link_new_device_enter_number_notice">"Kirjoita 2-numeroinen koodi"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Tämä varmistaa, että yhteys toiseen laitteeseesi on turvallinen."</string>
@@ -5,6 +5,7 @@
<string name="screen_link_new_device_desktop_step3">"Scanner le code QR avec cet appareil"</string>
<string name="screen_link_new_device_desktop_submit">"Prêt à scanner"</string>
<string name="screen_link_new_device_desktop_title">"Ouvrir %1$s sur un ordinateur pour obtenir le code QR"</string>
<string name="screen_link_new_device_desktop_warning">"Ne scannez un code QR qu’après avoir suivi les étapes ci-dessus sur votre propre appareil. Ne scannez jamais un code envoyé par quelqu’un d’autre."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Les nombres ne correspondent pas"</string>
<string name="screen_link_new_device_enter_number_notice">"Saisissez le code à 2 chiffres"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Cela permettra de vérifier que la connexion à votre autre appareil est sécurisée."</string>
+1
View File
@@ -71,6 +71,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.wellknown.api)
implementation(projects.features.enterprise.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
@@ -8,7 +8,7 @@
package io.element.android.features.location.api
import androidx.compose.runtime.staticCompositionLocalOf
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Provides a [MapTilerConfig] for the MapLibre maps.
@@ -37,6 +37,7 @@ import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
@@ -45,7 +46,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Shows a static map image downloaded via a third party service's static maps API.
@@ -8,8 +8,8 @@
package io.element.android.features.location.api.internal
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlin.math.roundToInt
/**
@@ -10,8 +10,8 @@
package io.element.android.features.location.api.internal
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
internal class MapTilerTileServerStyleUriBuilder(
private val baseUrl: String,
@@ -10,8 +10,7 @@ package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Builds an URL for a 3rd party service provider static maps API.
*/
@@ -11,7 +11,7 @@ package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Builds a style URI for a MapLibre compatible tile server.
@@ -9,7 +9,7 @@
package io.element.android.features.location.api.internal
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import org.junit.Test
class MapTilerTileServerStyleUriBuilderTest {
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.libraries.architecture.AsyncData
@@ -50,7 +51,6 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.wellknown.api.MapTilerConfig
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.map.MapOptions
@@ -22,7 +22,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.location.impl.common.LocationConstraintsCheck
import io.element.android.features.location.impl.common.MapDefaults
@@ -73,7 +73,7 @@ class ShareLocationPresenter(
private val liveLocationShareManager: ActiveLiveLocationShareManager,
private val liveLocationStore: LiveLocationStore,
private val userLocationStateFactory: UserLocationState.Factory,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : Presenter<ShareLocationState> {
@AssistedFactory
fun interface Factory {
@@ -96,7 +96,7 @@ class ShareLocationPresenter(
val currentUser by client.userProfile.collectAsState()
val customMapConfig by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig)
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
}
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
perms.sendLiveLocationPermissions()
@@ -8,12 +8,12 @@
package io.element.android.features.location.impl.share
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.userlocation.UserLocationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.ImmutableList
data class ShareLocationState(
@@ -9,13 +9,13 @@
package io.element.android.features.location.impl.share
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.userlocation.UserLocationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.persistentListOf
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
@@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
@@ -47,7 +47,6 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
@@ -58,18 +57,18 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@AssistedInject class ShowLocationPresenter(
@AssistedInject
class ShowLocationPresenter(
@Assisted private val mode: ShowLocationMode,
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
private val dateFormatter: DateFormatter,
private val stringProvider: StringProvider,
private val sessionId: SessionId,
private val joinedRoom: JoinedRoom,
private val liveLocationShareManager: ActiveLiveLocationShareManager,
private val userLocationStateFactory: UserLocationState.Factory,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : Presenter<ShowLocationState> {
@AssistedFactory fun interface Factory {
fun create(mode: ShowLocationMode): ShowLocationPresenter
@@ -89,7 +88,7 @@ import kotlinx.coroutines.launch
val customMapTilerConfig by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig)
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
}
fun checkLocationConstraints() {

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