mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge branch 'release/26.08.2'
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
=============================
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+1
-1
Submodule enterprise updated: f5f910856c...c87b257dd8
@@ -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>
|
||||
|
||||
+19
@@ -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(
|
||||
|
||||
+3
@@ -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).
|
||||
|
||||
+3
@@ -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>
|
||||
|
||||
+6
@@ -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)
|
||||
}
|
||||
|
||||
+12
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+61
-16
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -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
|
||||
}
|
||||
+16
-2
@@ -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()
|
||||
}
|
||||
|
||||
+15
@@ -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?>
|
||||
}
|
||||
+25
@@ -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?,
|
||||
)
|
||||
-17
@@ -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
|
||||
-23
@@ -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
|
||||
}
|
||||
}
|
||||
+46
@@ -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
|
||||
}
|
||||
}
|
||||
+4
-10
@@ -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
|
||||
}
|
||||
|
||||
+17
@@ -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
|
||||
}
|
||||
+18
@@ -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)
|
||||
}
|
||||
-30
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -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
|
||||
|
||||
+10
-17
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
-21
@@ -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)
|
||||
}
|
||||
}
|
||||
-17
@@ -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
|
||||
}
|
||||
+22
@@ -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>
|
||||
|
||||
+5
@@ -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>
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -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()
|
||||
}
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+13
@@ -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,
|
||||
|
||||
+7
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inviteData: InviteData,
|
||||
callback: DeclineInviteAndBlockEntryPoint.Callback,
|
||||
): Node {
|
||||
lambdaError()
|
||||
}
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+12
@@ -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>
|
||||
|
||||
+4
-1
@@ -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>
|
||||
|
||||
+9
@@ -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>
|
||||
|
||||
+3
@@ -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
|
||||
}
|
||||
|
||||
+10
@@ -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,
|
||||
|
||||
+4
@@ -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>
|
||||
|
||||
+11
-2
@@ -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")
|
||||
|
||||
+26
@@ -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(
|
||||
|
||||
+29
-3
@@ -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
Reference in New Issue
Block a user