Merge branch 'release/26.08.2'

This commit is contained in:
Benoit Marty
2026-08-20 10:12:55 +02:00
663 changed files with 10326 additions and 5263 deletions
@@ -78,6 +78,10 @@ else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi
git add -A
if git diff --cached --quiet; then
echo "Screenshots are already up to date, nothing to commit."
exit 0
fi
git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
-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
+92
View File
@@ -1,3 +1,95 @@
Changes in Element X v26.08.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
## What's Changed
### 🙌 Improvements
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
### 🐛 Bugfixes
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
### 🧱 Build
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
### 🚧 In development 🚧
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
### Dependency upgrades
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
### 💼 Element Pro
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
### Others
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
## New Contributors
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
Changes in Element X v26.08.0
=============================
+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"
}
@@ -35,4 +35,11 @@ object RageshakeConfig {
* The maximum number of log lines a rageshake can contain.
*/
const val MAX_LOG_LINES_SIZE = 1_000_000
/**
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
* mainly the list of the omitted log file names.
*/
const val MAX_DESCRIPTION_SIZE = 40 * 1024
}
+1
View File
@@ -44,6 +44,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.wellknown.api)
implementation(projects.features.login.api)
implementation(libs.coil)
@@ -11,8 +11,10 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
@@ -49,11 +51,13 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.location.api.LocalMapTilerConfig
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -153,6 +157,7 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -677,6 +682,12 @@ class LoggedInFlowNode(
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
val currentMapTilerConfig = LocalMapTilerConfig.current
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
}
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
featureFlagService = featureFlagService,
@@ -684,16 +695,18 @@ class LoggedInFlowNode(
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) {
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
}
}
@@ -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(
+8
View File
@@ -1,3 +1,5 @@
import org.sonarqube.gradle.SonarResolverTask
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
@@ -18,3 +20,9 @@ dependencies {
implementation(libs.ksp.plugin)
implementation(libs.kotlinpoet.ksp)
}
// Ensure that the processResources task is executed before the SonarResolverTask to avoid issues with missing resources during SonarQube analysis.
// This *should not be necesssary* but is a workaround for a known issue with the SonarQube Gradle plugin.
tasks.withType<SonarResolverTask>().configureEach {
dependsOn("processResources")
}
@@ -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>
@@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.Flow
/**
* Queues the full-screen announcements shown to the user once, such as the notice about a new feature.
*
* A dismissed announcement is remembered so it is not shown again.
*/
interface AnnouncementService {
/**
* Queues an announcement, unless the user has already dismissed it.
*
* @param announcement the announcement to show.
*/
suspend fun showAnnouncement(announcement: Announcement)
/**
* Records that the user dismissed an announcement, so it is not queued again.
*
* @param announcement the announcement that was dismissed.
*/
suspend fun onAnnouncementDismissed(announcement: Announcement)
/** The announcements still waiting to be shown, in the order they should appear. */
fun announcementsToShowFlow(): Flow<List<Announcement>>
/**
* Use this composable to render the announcement UI in Fullscreen.
* Draws nothing while there is no announcement to show.
*
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
@@ -8,6 +8,9 @@
package io.element.android.features.cachecleaner.api
/**
* Removes the temporary files holding decrypted content, so plaintext media does not linger on disk.
*/
interface CacheCleaner {
/**
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
@@ -10,6 +10,9 @@ package io.element.android.features.call.api
import kotlinx.coroutines.flow.StateFlow
/**
* Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it.
*/
interface CurrentCallService {
/**
* The current call state flow, which will be updated when the active call changes.
@@ -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>
@@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
*/
fun interface ContentScannerService {
/**
* Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning.
*
* @param mediaSources the media to scan.
* @param contentValidationState where the outcome of each scan is published for the UI to observe.
*/
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
}
@@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
/**
* Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens.
*/
interface Builder {
/**
* @param isSpace true to create a space rather than a room.
*/
fun setIsSpace(isSpace: Boolean): Builder
/**
* @param parentSpaceId the space the new room should be added to once created.
*/
fun setParentSpace(parentSpaceId: RoomId): Builder
/** Builds the node with the options configured so far. */
fun build(): Node
}
@@ -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,18 +10,52 @@ 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
/**
* Exposes the restrictions and customisations an enterprise deployment can impose: allowed homeservers, branding, push gateways and more.
*
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
*/
interface EnterpriseService {
val isEnterpriseBuild: Boolean
/**
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
*
* @param sessionId the session to check.
*/
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
fun defaultHomeserverList(): List<String>
/**
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
*
* @param url the URL to rewrite.
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
*/
suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String
/**
* 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 homeserverAllowList(): List<String>
/**
* 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.
@@ -29,37 +63,48 @@ interface EnterpriseService {
*/
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
/**
* The brand color to apply, or `null` when the default theme color should be used.
*
* @param sessionId the session to read the color of, or `null` for the color used when no session is active.
*/
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
/**
* The full light and dark color palettes derived from the brand color.
*
* @param sessionId the session to read the colors of, or `null` for the colors used when no session is active.
*/
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
/** The push gateway to register with when using Firebase, or `null` to use the app default. */
fun firebasePushGateway(): String?
/** The push gateway to register with when using UnifiedPush, or `null` to use the app default. */
fun unifiedPushDefaultPushGateway(): String?
/**
* Where bug reports should be submitted, which an enterprise deployment can redirect or disable entirely.
*
* @param sessionId the session to read the setting of, or `null` for the setting used when no session is active.
*/
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
/**
* Gets Notification Channel to use for the noisy notifications of the provided session.
* Returns `null` when the session has no dedicated channel and the app-wide one should be used.
*
* @param sessionId the session whose channel is requested.
*/
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.
*/
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
}
@@ -8,11 +8,25 @@
package io.element.android.features.enterprise.api
/**
* The session-scoped counterpart of [EnterpriseService], answering the same kind of questions for the session it belongs to.
*
* [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
/**
* Rewrites the authentication server URL for this session; see [EnterpriseService.tweakMasUrl].
*
* @param url the URL to rewrite.
*/
suspend fun tweakMasUrl(url: String): String
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
suspend fun isEncryptionDisabledByHomeserver(): Boolean
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?>
}
@@ -0,0 +1,25 @@
/*
* 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 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,
/** The style ID to use for light mode. This is optional and can be null. */
val lightStyleId: String?,
/** The style ID to use for light mode. This is optional and can be null. */
val darkStyleId: String?,
/** The base URL to use for MapTiler. This is optional and can be null. */
val baseUrl: 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,23 +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 {
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
interface Factory {
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,
@@ -11,6 +11,9 @@ package io.element.android.features.invite.api
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
/**
* Remembers which invitations the user has already looked at, so that only genuinely new ones are highlighted in the room list.
*/
interface SeenInvitesStore {
/**
* Returns a flow of seen room IDs of invitation.
@@ -32,7 +35,7 @@ interface SeenInvitesStore {
suspend fun markAsUnSeen(roomId: RoomId)
/**
* Delete the store.
* Delete the store, so every invitation counts as unseen again.
*/
suspend fun clear()
}
@@ -10,6 +10,9 @@ package io.element.android.features.invite.api.acceptdecline
import io.element.android.features.invite.api.InviteData
/**
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
*/
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
@@ -12,7 +12,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Renders the confirmation dialogs and error states of accepting or declining an invitation; it draws nothing until such an event is sent.
*
* Embedded by every screen that can act on an invitation, so the behaviour stays identical between the invite list and a room preview.
*/
fun interface AcceptDeclineInviteView {
/**
* Draws whichever dialog the current state calls for.
*
* @param state the state produced by the accept/decline presenter.
* @param onAcceptInviteSuccess called once the room has been joined, so the host screen can navigate into it.
* @param onDeclineInviteSuccess called once the invitation has been declined.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: AcceptDeclineInviteState,
@@ -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()
}
@@ -8,6 +8,9 @@
package io.element.android.features.invitepeople.api
/**
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
*/
interface InvitePeopleEvents {
data object SendInvites : InvitePeopleEvents
data object CloseSearch : InvitePeopleEvents
@@ -13,7 +13,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
/**
* Creates a presenter for one invite flow.
*/
interface Factory {
/**
* @param joinedRoom the room to invite into, or `null` when the room does not exist yet, as when inviting from a direct message.
* @param roomId the id of the room to invite into.
*/
fun create(
joinedRoom: JoinedRoom?,
roomId: RoomId,
@@ -11,7 +11,16 @@ package io.element.android.features.invitepeople.api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Renders the invite people UI, so that other screens can embed it without depending on its implementation.
*/
interface InvitePeopleRenderer {
/**
* Draws the invite list and its search field.
*
* @param state the state produced by [InvitePeoplePresenter].
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: InvitePeopleState,
@@ -11,10 +11,22 @@ package io.element.android.features.invitepeople.api
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
/**
* State of the invite people UI, produced by an [InvitePeoplePresenter] and consumed by an [InvitePeopleRenderer].
*/
interface InvitePeopleState {
/** Whether the send button should be enabled, i.e. at least one user is selected and the user has the permission. */
val canInvite: Boolean
/** Whether the search field currently has focus, which the host screen uses to adapt its own layout. */
val isSearchActive: Boolean
/** Progress of the invite request, so the UI can show a loader and surface failures. */
val sendInvitesAction: AsyncAction<Unit>
/** Progress of turning a direct message into a room, which happens when inviting a third person into a DM. */
val createRoomFromDmAction: AsyncAction<RoomId>
/** Where the UI sends its events. */
val eventSink: (InvitePeopleEvents) -> Unit
}
@@ -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>
@@ -11,7 +11,16 @@ package io.element.android.features.knockrequests.api.banner
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Renders the banner shown at the top of a room when people are waiting to join it.
*/
interface KnockRequestsBannerRenderer {
/**
* Draws the banner, or nothing when there is no pending request or the user cannot moderate the room.
*
* @param modifier layout modifier for the container.
* @param onViewRequestsClick called when the user asks to see the full list of requests.
*/
@Composable
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
}
@@ -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>
@@ -10,6 +10,9 @@ package io.element.android.features.leaveroom.api
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Events sent to the leave room presenter through [LeaveRoomState.eventSink].
*/
interface LeaveRoomEvent {
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
}
@@ -12,7 +12,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Renders the confirmation dialogs of the leave room flow; it draws nothing until the host screen sends a leave event.
*/
fun interface LeaveRoomRenderer {
/**
* Draws whichever dialog the current state calls for.
*
* @param state the state produced by the leave room presenter.
* @param onSelectNewOwners called when the user is the last owner of a room that still has other members, and must promote someone before leaving.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: LeaveRoomState,
@@ -10,7 +10,11 @@ package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Immutable
/**
* State of the leave room flow, which is only the confirmation dialogs: the screen it is hosted in owns the rest of the UI.
*/
@Immutable
interface LeaveRoomState {
/** Where the host screen sends its events to start or confirm leaving a room. */
val eventSink: (LeaveRoomEvent) -> Unit
}
@@ -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

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