Merge branch 'develop' into feature/valere/support_verification_violation_banner

This commit is contained in:
Benoit Marty 2025-02-18 15:42:08 +01:00 committed by GitHub
commit 36d5528904
625 changed files with 6274 additions and 2292 deletions

View file

@ -20,7 +20,7 @@ jobs:
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger
uses: danger/danger-js@12.3.3 uses: danger/danger-js@12.3.4
with: with:
args: "--dangerfile ./tools/danger/dangerfile.js" args: "--dangerfile ./tools/danger/dangerfile.js"
env: env:

View file

@ -294,7 +294,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@12.3.3 uses: danger/danger-js@12.3.4
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

View file

@ -14,7 +14,6 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -26,6 +25,7 @@ import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
@ -61,7 +61,10 @@ class MainActivity : NodeActivity() {
@Composable @Composable
private fun MainContent(appBindings: AppBindings) { private fun MainContent(appBindings: AppBindings) {
val migrationState = appBindings.migrationEntryPoint().present() val migrationState = appBindings.migrationEntryPoint().present()
ElementThemeApp(appBindings.preferencesStore()) { ElementThemeApp(
appPreferencesStore = appBindings.preferencesStore(),
enterpriseService = appBindings.enterpriseService(),
) {
CompositionLocalProvider( CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this), LocalUriHandler provides SafeUriHandler(this),
@ -69,8 +72,8 @@ class MainActivity : NodeActivity() {
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(ElementTheme.colors.bgCanvasDefault),
) { ) {
if (migrationState.migrationAction.isSuccess()) { if (migrationState.migrationAction.isSuccess()) {
MainNodeHost() MainNodeHost()

View file

@ -9,6 +9,7 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
@ -35,4 +36,6 @@ interface AppBindings {
fun lockScreenEntryPoint(): LockScreenEntryPoint fun lockScreenEntryPoint(): LockScreenEntryPoint
fun analyticsService(): AnalyticsService fun analyticsService(): AnalyticsService
fun enterpriseService(): EnterpriseService
} }

View file

@ -25,6 +25,7 @@
<locale android:name="ru"/> <locale android:name="ru"/>
<locale android:name="sk"/> <locale android:name="sk"/>
<locale android:name="sv"/> <locale android:name="sv"/>
<locale android:name="tr"/>
<locale android:name="uk"/> <locale android:name="uk"/>
<locale android:name="uz"/> <locale android:name="uz"/>
<locale android:name="zh-CN"/> <locale android:name="zh-CN"/>

View file

@ -1,8 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Ref: https://developer.android.com/training/articles/security-config.html --> <!-- Ref: https://developer.android.com/training/articles/security-config.html -->
<!-- By default, do not allow clearText traffic --> <!-- By default, do not allow clearText traffic -->
<base-config cleartextTrafficPermitted="false" /> <base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
<!-- Allow clearText traffic on some specified host --> <!-- Allow clearText traffic on some specified host -->
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
@ -24,12 +31,4 @@
<domain includeSubdomains="true">lan</domain> <domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</domain> <domain includeSubdomains="true">localdomain</domain>
</domain-config> </domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config> </network-security-config>

View file

@ -14,9 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.composable.PermanentChild import com.bumble.appyx.core.composable.PermanentChild
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -52,8 +50,6 @@ 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.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
@ -77,18 +73,13 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -107,7 +98,6 @@ class LoggedInFlowNode @AssistedInject constructor(
private val userProfileEntryPoint: UserProfileEntryPoint, private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint, private val ftueEntryPoint: FtueEntryPoint,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
private val ftueService: FtueService, private val ftueService: FtueService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint, private val shareEntryPoint: ShareEntryPoint,
@ -133,7 +123,6 @@ class LoggedInFlowNode @AssistedInject constructor(
fun onOpenBugReport() fun onOpenBugReport()
} }
private val syncService = matrixClient.syncService()
private val loggedInFlowProcessor = LoggedInEventProcessor( private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher, snackbarDispatcher,
matrixClient.roomMembershipObserver(), matrixClient.roomMembershipObserver(),
@ -147,6 +136,7 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
@ -165,12 +155,6 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
}, },
onStop = {
coroutineScope.launch {
// Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
syncService.stopSync()
}
},
onDestroy = { onDestroy = {
appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id) appNavigationStateService.onLeavingSession(id)
@ -178,7 +162,6 @@ class LoggedInFlowNode @AssistedInject constructor(
matrixClient.sessionVerificationService().setListener(null) matrixClient.sessionVerificationService().setListener(null)
} }
) )
observeSyncStateAndNetworkStatus()
setupSendingQueue() setupSendingQueue()
} }
@ -186,31 +169,6 @@ class LoggedInFlowNode @AssistedInject constructor(
sendingQueue.launchIn(lifecycleScope) sendingQueue.launchIn(lifecycleScope)
} }
@OptIn(FlowPreview::class)
private fun observeSyncStateAndNetworkStatus() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
// small debounce to avoid spamming startSync when the state is changing quickly in case of error.
syncService.syncState.debounce(100),
networkMonitor.connectivity
) { syncState, networkStatus ->
Pair(syncState, networkStatus)
}
.onStart {
// Temporary fix to ensure that the sync is started even if the networkStatus is offline.
syncService.startSync()
}
.collect { (syncState, networkStatus) ->
Timber.d("Sync state: $syncState, network status: $networkStatus")
if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
syncService.startSync()
}
}
}
}
}
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Placeholder : NavTarget data object Placeholder : NavTarget

View file

@ -27,7 +27,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootNavStateFlowFactory
@ -62,7 +62,7 @@ class RootFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val navStateFlowFactory: RootNavStateFlowFactory, private val navStateFlowFactory: RootNavStateFlowFactory,
private val matrixClientsHolder: MatrixClientsHolder, private val matrixSessionCache: MatrixSessionCache,
private val presenter: RootPresenter, private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint, private val bugReportEntryPoint: BugReportEntryPoint,
private val viewFolderEntryPoint: ViewFolderEntryPoint, private val viewFolderEntryPoint: ViewFolderEntryPoint,
@ -78,14 +78,14 @@ class RootFlowNode @AssistedInject constructor(
plugins = plugins plugins = plugins
) { ) {
override fun onBuilt() { override fun onBuilt() {
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap) matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
super.onBuilt() super.onBuilt()
observeNavState() observeNavState()
} }
override fun onSaveInstanceState(state: MutableSavedStateMap) { override fun onSaveInstanceState(state: MutableSavedStateMap) {
super.onSaveInstanceState(state) super.onSaveInstanceState(state)
matrixClientsHolder.saveIntoSavedState(state) matrixSessionCache.saveIntoSavedState(state)
navStateFlowFactory.saveIntoSavedState(state) navStateFlowFactory.saveIntoSavedState(state)
} }
@ -118,7 +118,7 @@ class RootFlowNode @AssistedInject constructor(
} }
private fun switchToNotLoggedInFlow() { private fun switchToNotLoggedInFlow() {
matrixClientsHolder.removeAll() matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow) backstack.safeRoot(NavTarget.NotLoggedInFlow)
} }
@ -131,7 +131,7 @@ class RootFlowNode @AssistedInject constructor(
onFailure: () -> Unit, onFailure: () -> Unit,
onSuccess: (SessionId) -> Unit, onSuccess: (SessionId) -> Unit,
) { ) {
matrixClientsHolder.getOrRestore(sessionId) matrixSessionCache.getOrRestore(sessionId)
.onSuccess { .onSuccess {
Timber.v("Succeed to restore session $sessionId") Timber.v("Succeed to restore session $sessionId")
onSuccess(sessionId) onSuccess(sessionId)
@ -200,7 +200,7 @@ class RootFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.LoggedInFlow -> { is NavTarget.LoggedInFlow -> {
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
Timber.w("Couldn't find any session, go through SplashScreen") Timber.w("Couldn't find any session, go through SplashScreen")
} }
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)

View file

@ -7,6 +7,7 @@
package io.element.android.appnav.di package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.core.state.SavedStateMap
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
@ -25,45 +26,61 @@ import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
/**
* In-memory cache for logged in Matrix sessions.
*
* This component contains both the [MatrixClient] and the [SyncOrchestrator] for each session.
*/
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class MatrixClientsHolder @Inject constructor( class MatrixSessionCache @Inject constructor(
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val syncOrchestratorFactory: SyncOrchestrator.Factory,
) : MatrixClientProvider { ) : MatrixClientProvider {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>() private val sessionIdsToMatrixSession = ConcurrentHashMap<SessionId, InMemoryMatrixSession>()
private val restoreMutex = Mutex() private val restoreMutex = Mutex()
init { init {
authenticationService.listenToNewMatrixClients { matrixClient -> authenticationService.listenToNewMatrixClients { matrixClient ->
sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator,
)
syncOrchestrator.start()
} }
} }
fun removeAll() { fun removeAll() {
sessionIdsToMatrixClient.clear() sessionIdsToMatrixSession.clear()
} }
fun remove(sessionId: SessionId) { fun remove(sessionId: SessionId) {
sessionIdsToMatrixClient.remove(sessionId) sessionIdsToMatrixSession.remove(sessionId)
} }
override fun getOrNull(sessionId: SessionId): MatrixClient? { override fun getOrNull(sessionId: SessionId): MatrixClient? {
return sessionIdsToMatrixClient[sessionId] return sessionIdsToMatrixSession[sessionId]?.matrixClient
} }
override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> { override suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient> {
return restoreMutex.withLock { return restoreMutex.withLock {
when (val matrixClient = getOrNull(sessionId)) { when (val cached = getOrNull(sessionId)) {
null -> restore(sessionId) null -> restore(sessionId)
else -> Result.success(matrixClient) else -> Result.success(cached)
} }
} }
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getSyncOrchestrator(sessionId: SessionId): SyncOrchestrator? {
return sessionIdsToMatrixSession[sessionId]?.syncOrchestrator
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun restoreWithSavedState(state: SavedStateMap?) { fun restoreWithSavedState(state: SavedStateMap?) {
Timber.d("Restore state") Timber.d("Restore state")
if (state == null || sessionIdsToMatrixClient.isNotEmpty()) { if (state == null || sessionIdsToMatrixSession.isNotEmpty()) {
Timber.w("Restore with non-empty map") Timber.w("Restore with non-empty map")
return return
} }
@ -79,7 +96,7 @@ class MatrixClientsHolder @Inject constructor(
} }
fun saveIntoSavedState(state: MutableSavedStateMap) { fun saveIntoSavedState(state: MutableSavedStateMap) {
val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() val sessionKeys = sessionIdsToMatrixSession.keys.toTypedArray()
Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}")
state[SAVE_INSTANCE_KEY] = sessionKeys state[SAVE_INSTANCE_KEY] = sessionKeys
} }
@ -88,10 +105,20 @@ class MatrixClientsHolder @Inject constructor(
Timber.d("Restore matrix session: $sessionId") Timber.d("Restore matrix session: $sessionId")
return authenticationService.restoreSession(sessionId) return authenticationService.restoreSession(sessionId)
.onSuccess { matrixClient -> .onSuccess { matrixClient ->
sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient val syncOrchestrator = syncOrchestratorFactory.create(matrixClient)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator,
)
syncOrchestrator.start()
} }
.onFailure { .onFailure {
Timber.e(it, "Fail to restore session") Timber.e(it, "Fail to restore session")
} }
} }
} }
private data class InMemoryMatrixSession(
val matrixClient: MatrixClient,
val syncOrchestrator: SyncOrchestrator,
)

View file

@ -0,0 +1,127 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class SyncOrchestrator @AssistedInject constructor(
@Assisted matrixClient: MatrixClient,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
dispatchers: CoroutineDispatchers,
) {
@AssistedFactory
interface Factory {
fun create(matrixClient: MatrixClient): SyncOrchestrator
}
private val syncService = matrixClient.syncService()
private val tag = "SyncOrchestrator"
private val coroutineScope = matrixClient.sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
/**
* Starting observing the app state and network state to start/stop the sync service.
*
* Before observing the state, a first attempt at starting the sync service will happen if it's not already running.
*/
fun start() {
if (!started.compareAndSet(false, true)) {
Timber.tag(tag).d("already started, exiting early")
return
}
coroutineScope.launch {
// Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible
// Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline
Timber.tag(tag).d("performing initial sync attempt")
syncService.startSync()
// Wait until the sync service is not idle, either it will be running or in error/offline state
syncService.syncState.first { it != SyncState.Idle }
observeStates()
}
}
@OptIn(FlowPreview::class)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun observeStates() = coroutineScope.launch {
Timber.tag(tag).d("start observing the app and network state")
combine(
// small debounce to avoid spamming startSync when the state is changing quickly in case of error.
syncService.syncState.debounce(100.milliseconds),
networkMonitor.connectivity,
appForegroundStateService.isInForeground,
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
) { syncState, networkState, isInForeground, isInCall, isSyncingNotificationEvent ->
val isAppActive = isInForeground || isInCall || isSyncingNotificationEvent
val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
if (syncState == SyncState.Running && !isAppActive) {
SyncStateAction.StopSync
} else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) {
SyncStateAction.StartSync
} else {
SyncStateAction.NoOp
}
}
.distinctUntilChanged()
.debounce { action ->
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
if (action == SyncStateAction.StopSync) 3.seconds else 0.seconds
}
.onCompletion {
Timber.tag(tag).d("has been stopped")
}
.collect { action ->
when (action) {
SyncStateAction.StartSync -> {
syncService.startSync()
}
SyncStateAction.StopSync -> {
syncService.stopSync()
}
SyncStateAction.NoOp -> Unit
}
}
}
}
private enum class SyncStateAction {
StartSync,
StopSync,
NoOp,
}

View file

@ -32,7 +32,7 @@ class SendQueues @Inject constructor(
) { ) {
/** /**
* Launches the send queues retry mechanism in the given [coroutineScope]. * Launches the send queues retry mechanism in the given [coroutineScope].
* Makes sure to re-enable all send queues when the network status is [NetworkStatus.Online]. * Makes sure to re-enable all send queues when the network status is [NetworkStatus.Connected].
*/ */
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
fun launchIn(coroutineScope: CoroutineScope) { fun launchIn(coroutineScope: CoroutineScope) {

View file

@ -9,7 +9,7 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.core.state.SavedStateMap
import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.features.login.api.LoginUserStory import io.element.android.features.login.api.LoginUserStory
import io.element.android.features.preferences.api.CacheService import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -31,7 +31,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact
class RootNavStateFlowFactory @Inject constructor( class RootNavStateFlowFactory @Inject constructor(
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val cacheService: CacheService, private val cacheService: CacheService,
private val matrixClientsHolder: MatrixClientsHolder, private val matrixSessionCache: MatrixSessionCache,
private val imageLoaderHolder: ImageLoaderHolder, private val imageLoaderHolder: ImageLoaderHolder,
private val loginUserStory: LoginUserStory, private val loginUserStory: LoginUserStory,
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory, private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
@ -63,7 +63,7 @@ class RootNavStateFlowFactory @Inject constructor(
val initialCacheIndex = savedStateMap.getCacheIndexOrDefault() val initialCacheIndex = savedStateMap.getCacheIndexOrDefault()
return cacheService.clearedCacheEventFlow return cacheService.clearedCacheEventFlow
.onEach { sessionId -> .onEach { sessionId ->
matrixClientsHolder.remove(sessionId) matrixSessionCache.remove(sessionId)
// Ensure image loader will be recreated with the new MatrixClient // Ensure image loader will be recreated with the new MatrixClient
imageLoaderHolder.remove(sessionId) imageLoaderHolder.remove(sessionId)
// Also remove cached value for SessionPreferencesStore // Also remove cached value for SessionPreferencesStore

View file

@ -0,0 +1,5 @@
<?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">ıkış Yap ve Yükselt"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ana sunucunuz artık eski protokolü desteklemiyor. Lütfen oturumu kapatın ve uygulamayı kullanmaya devam etmek için tekrar oturum açın."</string>
</resources>

View file

@ -0,0 +1,349 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav
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.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class)
class SyncOrchestratorTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `when the sync wasn't running before, an initial sync will take place, even with no network`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
)
// We start observing with an initial sync
syncOrchestrator.start()
// Advance the time just enough to make sure the initial sync has run
advanceTimeBy(1.milliseconds)
startSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the sync wasn't running before, an initial sync will take place`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
)
// We start observing with an initial sync
syncOrchestrator.start()
// Advance the time just enough to make sure the initial sync has run
advanceTimeBy(1.milliseconds)
startSyncRecorder.assertions().isCalledOnce()
// If we wait for a while, the sync will not be started again by the observer since it's already running
advanceTimeBy(10.seconds)
startSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the app goes to background and the sync was running, it will be stopped after a delay`() = runTest {
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply {
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// Stop sync was never called
stopSyncRecorder.assertions().isNeverCalled()
// Now we send the app to background
appForegroundStateService.isInForeground.value = false
// Stop sync will be called after some delay
stopSyncRecorder.assertions().isNeverCalled()
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the app state changes several times in a short while, stop sync is only called once`() = runTest {
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply {
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// Stop sync was never called
stopSyncRecorder.assertions().isNeverCalled()
// Now we send the app to background
appForegroundStateService.isInForeground.value = false
// Ensure the stop action wasn't called yet
stopSyncRecorder.assertions().isNeverCalled()
advanceTimeBy(1.seconds)
appForegroundStateService.isInForeground.value = true
advanceTimeBy(1.seconds)
// Ensure the stop action wasn't called yet either, since we didn't give it enough time to emit after the expected delay
stopSyncRecorder.assertions().isNeverCalled()
// Now change it again and wait for enough time
appForegroundStateService.isInForeground.value = false
advanceTimeBy(4.seconds)
// And confirm it's now called
stopSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the app was in background and we receive a notification, a sync will be started then stopped`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false,
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// Start sync was never called
startSyncRecorder.assertions().isNeverCalled()
// Now we receive a notification and need to sync
appForegroundStateService.updateIsSyncingNotificationEvent(true)
// Start sync will be called shortly after
advanceTimeBy(1.milliseconds)
startSyncRecorder.assertions().isCalledOnce()
// If the sync is running and we mark the notification sync as no longer necessary, the sync stops after a delay
syncService.emitSyncState(SyncState.Running)
appForegroundStateService.updateIsSyncingNotificationEvent(false)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the app was in background and we join a call, a sync will be started`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false,
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// Start sync was never called
startSyncRecorder.assertions().isNeverCalled()
// Now we join a call
appForegroundStateService.updateIsInCallState(true)
// Start sync will be called shortly after
advanceTimeBy(1.milliseconds)
startSyncRecorder.assertions().isCalledOnce()
// If the sync is running and we mark the in-call state as false, the sync stops after a delay
syncService.emitSyncState(SyncState.Running)
appForegroundStateService.updateIsInCallState(false)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `when the app is in foreground, we sync for a notification and a call is ongoing, the sync will only stop when all conditions are false`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply {
startSyncLambda = startSyncRecorder
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = true,
initialIsSyncingNotificationEventValue = true,
initialIsInCallValue = true,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// Start sync was never called
startSyncRecorder.assertions().isNeverCalled()
// We send the app to background, it's still syncing
appForegroundStateService.givenIsInForeground(false)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isNeverCalled()
// We stop the notification sync, it's still syncing
appForegroundStateService.updateIsSyncingNotificationEvent(false)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isNeverCalled()
// We set the in-call state to false, now it stops syncing after a delay
appForegroundStateService.updateIsInCallState(false)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isCalledOnce()
}
@Test
fun `if the sync was running, it's set to be stopped but something triggers a sync again, the sync is not stopped`() = runTest {
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply {
stopSyncLambda = stopSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = true,
initialIsSyncingNotificationEventValue = false,
initialIsInCallValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// Advance the time to make sure the orchestrator has had time to start processing the inputs
advanceTimeBy(100.milliseconds)
// This will set the sync to stop
appForegroundStateService.givenIsInForeground(false)
// But if we reset it quickly before the stop sync takes place, the sync is not stopped
advanceTimeBy(2.seconds)
appForegroundStateService.givenIsInForeground(true)
advanceTimeBy(10.seconds)
stopSyncRecorder.assertions().isNeverCalled()
}
@Test
fun `when network is offline, sync service should not start`() = runTest {
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
networkMonitor = networkMonitor,
)
// We start observing
syncOrchestrator.observeStates()
// This should still not trigger a sync, since there is no network
advanceTimeBy(10.seconds)
startSyncRecorder.assertions().isNeverCalled()
}
private fun TestScope.createSyncOrchestrator(
syncService: FakeSyncService = FakeSyncService(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
matrixClient = FakeMatrixClient(syncService = syncService, sessionCoroutineScope = backgroundScope),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
dispatchers = testCoroutineDispatchers(),
)
}

View file

@ -1,97 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat
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
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MatrixClientsHolderTest {
@Test
fun `test getOrNull`() {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
}
@Test
fun `test getOrRestore`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
// Do it again to hit the cache
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
}
@Test
fun `test remove`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
// Remove
matrixClientsHolder.remove(A_SESSION_ID)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
}
@Test
fun `test remove all`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
// Remove all
matrixClientsHolder.removeAll()
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
}
@Test
fun `test save and restore`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
val fakeMatrixClient = FakeMatrixClient()
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
matrixClientsHolder.getOrRestore(A_SESSION_ID)
val savedStateMap = MutableSavedStateMapImpl { true }
matrixClientsHolder.saveIntoSavedState(savedStateMap)
assertThat(savedStateMap.size).isEqualTo(1)
// Test Restore with non-empty map
matrixClientsHolder.restoreWithSavedState(savedStateMap)
// Empty the map
matrixClientsHolder.removeAll()
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
// Restore again
matrixClientsHolder.restoreWithSavedState(savedStateMap)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
}
@Test
fun `test AuthenticationService listenToNewMatrixClients emits a Client value and we save it`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
fakeAuthenticationService.givenMatrixClient(FakeMatrixClient(sessionId = A_SESSION_ID))
val loginSucceeded = fakeAuthenticationService.login("user", "pass")
assertThat(loginSucceeded.isSuccess).isTrue()
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNotNull()
}
}

View file

@ -0,0 +1,131 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.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.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
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MatrixSessionCacheTest {
@Test
fun `test getOrNull`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `test getSyncOrchestratorOrNull`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
// With no matrix client there is no sync orchestrator
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
assertThat(matrixSessionCache.getSyncOrchestrator(A_SESSION_ID)).isNull()
// But as soon as we receive a client, we can get the sync orchestrator
val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope)
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixSessionCache.getSyncOrchestrator(A_SESSION_ID)).isNotNull()
}
@Test
fun `test getOrRestore`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope)
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
// Do it again to hit the cache
assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
}
@Test
fun `test remove`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope)
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
// Remove
matrixSessionCache.remove(A_SESSION_ID)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
}
@Test
fun `test remove all`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope)
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
// Remove all
matrixSessionCache.removeAll()
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
}
@Test
fun `test save and restore`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope)
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
matrixSessionCache.getOrRestore(A_SESSION_ID)
val savedStateMap = MutableSavedStateMapImpl { true }
matrixSessionCache.saveIntoSavedState(savedStateMap)
assertThat(savedStateMap.size).isEqualTo(1)
// Test Restore with non-empty map
matrixSessionCache.restoreWithSavedState(savedStateMap)
// Empty the map
matrixSessionCache.removeAll()
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
// Restore again
matrixSessionCache.restoreWithSavedState(savedStateMap)
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
}
@Test
fun `test AuthenticationService listenToNewMatrixClients emits a Client value and we save it`() = runTest {
val fakeAuthenticationService = FakeMatrixAuthenticationService()
val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory())
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull()
fakeAuthenticationService.givenMatrixClient(FakeMatrixClient(sessionId = A_SESSION_ID, sessionCoroutineScope = backgroundScope))
val loginSucceeded = fakeAuthenticationService.login("user", "pass")
assertThat(loginSucceeded.isSuccess).isTrue()
assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNotNull()
}
private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory {
override fun create(matrixClient: MatrixClient): SyncOrchestrator {
return SyncOrchestrator(
matrixClient,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),
dispatchers = testCoroutineDispatchers(),
)
}
}
}

@ -1 +1 @@
Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea Subproject commit 0c028db8a48118433c7e11737080a6a01fb90f69

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Sorunları tanımlamamıza yardımcı olmak için anonim kullanım verilerini paylaşın."</string>
<string name="screen_analytics_settings_read_terms">"Tüm şartlarımızı okuyabilirsiniz %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"burada"</string>
<string name="screen_analytics_settings_share_data">"Analitik verileri paylaşın"</string>
</resources>

View file

@ -8,9 +8,9 @@
package io.element.android.features.analytics.impl package io.element.android.features.analytics.impl
import android.app.Activity import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -34,7 +34,7 @@ class AnalyticsOptInNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not() val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present() val state = presenter.present()
AnalyticsOptInView( AnalyticsOptInView(

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment import androidx.compose.ui.BiasAlignment
@ -111,7 +110,7 @@ private fun AnalyticsOptInHeader(
.padding(8.dp), .padding(8.dp),
style = ElementTheme.typography.fontBodyMdRegular style = ElementTheme.typography.fontBodyMdRegular
.copy( .copy(
color = MaterialTheme.colorScheme.secondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
) )

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Hiçbir kişisel veriyi kaydetmeyeceğiz veya profillemeyeceğiz"</string>
<string name="screen_analytics_prompt_help_us_improve">"Sorunları tanımlamamıza yardımcı olmak için anonim kullanım verilerini paylaşın."</string>
<string name="screen_analytics_prompt_read_terms">"Tüm şartlarımızı okuyabilirsiniz %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"burada"</string>
<string name="screen_analytics_prompt_settings">"Bu özelliği istediğiniz zaman kapatabilirsiniz"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Verilerinizi üçüncü taraflarla paylaşmayacağız"</string>
<string name="screen_analytics_prompt_title">"%1$s geliştirilmesine yardımcı olun"</string>
</resources>

View file

@ -29,6 +29,7 @@ setupAnvil()
dependencies { dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
@ -40,6 +41,7 @@ dependencies {
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
implementation(libs.androidx.webkit) implementation(libs.androidx.webkit)
implementation(libs.coil.compose) implementation(libs.coil.compose)
@ -59,6 +61,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -58,9 +59,9 @@ class CallScreenPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val matrixClientsProvider: MatrixClientProvider, private val matrixClientsProvider: MatrixClientProvider,
private val screenTracker: ScreenTracker, private val screenTracker: ScreenTracker,
private val appCoroutineScope: CoroutineScope,
private val activeCallManager: ActiveCallManager, private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider, private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
) : Presenter<CallScreenState> { ) : Presenter<CallScreenState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -226,19 +227,13 @@ class CallScreenPresenter @AssistedInject constructor(
if (state == SyncState.Running) { if (state == SyncState.Running) {
client.notifyCallStartIfNeeded(callType.roomId) client.notifyCallStartIfNeeded(callType.roomId)
} else { } else {
client.syncService().startSync() appForegroundStateService.updateIsInCallState(true)
} }
} }
} }
onDispose { onDispose {
// We can't use the local coroutine scope here because it will be disposed before this effect // Make sure we mark the call as ended in the app state
appCoroutineScope.launch { appForegroundStateService.updateIsInCallState(false)
client.syncService().run {
if (syncState.value == SyncState.Running) {
stopSync()
}
}
}
} }
} }
} }

View file

@ -44,6 +44,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PipView import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
@ -61,6 +62,7 @@ class ElementCallActivity :
@Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var appPreferencesStore: AppPreferencesStore
@Inject lateinit var enterpriseService: EnterpriseService
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter @Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
private lateinit var presenter: Presenter<CallScreenState> private lateinit var presenter: Presenter<CallScreenState>
@ -109,7 +111,10 @@ class ElementCallActivity :
setContent { setContent {
val pipState = pictureInPicturePresenter.present() val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState) ListenToAndroidEvents(pipState)
ElementThemeApp(appPreferencesStore) { ElementThemeApp(
appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService,
) {
val state = presenter.present() val state = presenter.present()
eventSink = state.eventSink eventSink = state.eventSink
LaunchedEffect(state.isCallActive, state.isInWidgetMode) { LaunchedEffect(state.isCallActive, state.isInWidgetMode) {

View file

@ -19,6 +19,7 @@ import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.CallState
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -47,6 +48,9 @@ class IncomingCallActivity : AppCompatActivity() {
@Inject @Inject
lateinit var appPreferencesStore: AppPreferencesStore lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var enterpriseService: EnterpriseService
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -64,7 +68,10 @@ class IncomingCallActivity : AppCompatActivity() {
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
if (notificationData != null) { if (notificationData != null) {
setContent { setContent {
ElementThemeApp(appPreferencesStore) { ElementThemeApp(
appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService,
) {
IncomingCallScreen( IncomingCallScreen(
notificationData = notificationData, notificationData = notificationData,
onAnswer = ::onAnswer, onAnswer = ::onAnswer,

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Devam eden çağrı"</string>
<string name="call_foreground_service_message_android">"Aramaya geri dönmek için dokunun"</string>
<string name="call_foreground_service_title_android">"☎️ Çağrı devam ediyor"</string>
<string name="screen_incoming_call_subtitle_android">"Gelen Element Call"</string>
</resources>

View file

@ -32,10 +32,9 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -243,7 +242,7 @@ class CallScreenPresenterTest {
} }
@Test @Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
@ -251,6 +250,7 @@ class CallScreenPresenterTest {
this.startSyncLambda = startSyncLambda this.startSyncLambda = startSyncLambda
} }
val matrixClient = FakeMatrixClient(syncService = syncService) val matrixClient = FakeMatrixClient(syncService = syncService)
val appForegroundStateService = FakeAppForegroundStateService()
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
@ -258,34 +258,7 @@ class CallScreenPresenterTest {
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }), matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {}, screenTracker = FakeScreenTracker {},
) appForegroundStateService = appForegroundStateService,
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
consumeItemsUntilTimeout()
assert(startSyncLambda).isCalledOnce()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(SyncState.Running).apply {
this.stopSyncLambda = stopSyncLambda
}
val matrixClient = FakeMatrixClient(syncService = syncService)
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }),
screenTracker = FakeScreenTracker {},
) )
val hasRun = Mutex(true) val hasRun = Mutex(true)
val job = launch { val job = launch {
@ -296,11 +269,25 @@ class CallScreenPresenterTest {
} }
} }
hasRun.lock() appForegroundStateService.isInCall.test {
// The initial isInCall state will always be false
assertThat(awaitItem()).isFalse()
job.cancelAndJoin() // Wait until the call starts
hasRun.lock()
assert(stopSyncLambda).isCalledOnce() // Then it'll be true once the call is active
assertThat(awaitItem()).isTrue()
// If we dispose the screen
job.cancelAndJoin()
// The isInCall state is now false
assertThat(awaitItem()).isFalse()
// And there are no more events
ensureAllEventsConsumed()
}
} }
@Test @Test
@ -354,6 +341,7 @@ class CallScreenPresenterTest {
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(), screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
): CallScreenPresenter { ): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider { val userAgentProvider = object : UserAgentProvider {
override fun provide(): String { override fun provide(): String {
@ -369,10 +357,10 @@ class CallScreenPresenterTest {
clock = clock, clock = clock,
dispatchers = dispatchers, dispatchers = dispatchers,
matrixClientsProvider = matrixClientsProvider, matrixClientsProvider = matrixClientsProvider,
appCoroutineScope = this,
activeCallManager = activeCallManager, activeCallManager = activeCallManager,
screenTracker = screenTracker, screenTracker = screenTracker,
languageTagProvider = FakeLanguageTagProvider("en-US"), languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
) )
} }
} }

View file

@ -8,9 +8,9 @@
package io.element.android.features.createroom.impl.root package io.element.android.features.createroom.impl.root
import android.app.Activity import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -55,7 +55,7 @@ class CreateRoomRootNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val activity = LocalContext.current as Activity val activity = requireNotNull(LocalActivity.current)
CreateRoomRootView( CreateRoomRootView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -211,7 +210,7 @@ private fun CreateRoomActionButton(
) { ) {
Icon( Icon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.secondary, tint = ElementTheme.colors.iconSecondary,
resourceId = iconRes, resourceId = iconRes,
contentDescription = null, contentDescription = null,
) )

View file

@ -6,7 +6,7 @@
<string name="screen_create_room_private_option_description">"Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць."</string> <string name="screen_create_room_private_option_description">"Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць."</string>
<string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</string> <string name="screen_create_room_private_option_title">"Прыватны пакой (толькі па запрашэнні)"</string>
<string name="screen_create_room_public_option_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string> <string name="screen_create_room_public_option_description">"Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."</string>
<string name="screen_create_room_public_option_title">"Публічны пакой (для ўсіх)"</string> <string name="screen_create_room_public_option_title">"Публічны пакой"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Хто заўгодна"</string> <string name="screen_create_room_room_access_section_anyone_option_title">"Хто заўгодна"</string>
<string name="screen_create_room_room_access_section_header">"Доступ у пакой"</string> <string name="screen_create_room_room_access_section_header">"Доступ у пакой"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Папрасіце далучыцца"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Папрасіце далучыцца"</string>

View file

@ -6,7 +6,6 @@
<string name="screen_create_room_private_option_description">"Съобщенията в тази стая са шифровани. Шифроването не може да бъде изключено впоследствие."</string> <string name="screen_create_room_private_option_description">"Съобщенията в тази стая са шифровани. Шифроването не може да бъде изключено впоследствие."</string>
<string name="screen_create_room_private_option_title">"Частна стая (само с покана)"</string> <string name="screen_create_room_private_option_title">"Частна стая (само с покана)"</string>
<string name="screen_create_room_public_option_description">"Съобщенията не са шифровани и всеки може да ги прочете. Можете да активирате шифроването на по-късна дата."</string> <string name="screen_create_room_public_option_description">"Съобщенията не са шифровани и всеки може да ги прочете. Можете да активирате шифроването на по-късна дата."</string>
<string name="screen_create_room_public_option_title">"Публична стая (всеки)"</string>
<string name="screen_create_room_room_name_label">"Име на стаята"</string> <string name="screen_create_room_room_name_label">"Име на стаята"</string>
<string name="screen_create_room_title">"Създаване на стая"</string> <string name="screen_create_room_title">"Създаване на стая"</string>
<string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string> <string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string>

View file

@ -7,14 +7,14 @@
<string name="screen_create_room_private_option_title">"Privater Chatroom"</string> <string name="screen_create_room_private_option_title">"Privater Chatroom"</string>
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chatroom finden. <string name="screen_create_room_public_option_description">"Jeder kann diesen Chatroom finden.
Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string> Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Chatroom"</string> <string name="screen_create_room_public_option_title">"Öffentlicher Raum"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder kann diesem Chatroom beitreten"</string> <string name="screen_create_room_room_access_section_anyone_option_description">"Jeder kann diesem Chatroom beitreten"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Jemand"</string> <string name="screen_create_room_room_access_section_anyone_option_title">"Jemand"</string>
<string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</string> <string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
<string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string> <string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string>
<string name="screen_create_room_room_address_section_title">"Chatroom Adresse"</string> <string name="screen_create_room_room_address_section_title">"Chatroomadresse"</string>
<string name="screen_create_room_room_name_label">"Raumname"</string> <string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chatrooms"</string> <string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chatrooms"</string>
<string name="screen_create_room_title">"Raum erstellen"</string> <string name="screen_create_room_title">"Raum erstellen"</string>

View file

@ -14,7 +14,6 @@
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</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_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου."</string> <string name="screen_create_room_room_address_section_footer">"Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση δωματίου"</string>
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string> <string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα δωματίου"</string> <string name="screen_create_room_room_visibility_section_title">"Ορατότητα δωματίου"</string>
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string> <string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>

View file

@ -6,7 +6,6 @@
<string name="screen_create_room_private_option_description">"Los mensajes de esta sala están cifrados. La encriptación no se puede desactivar después."</string> <string name="screen_create_room_private_option_description">"Los mensajes de esta sala están cifrados. La encriptación no se puede desactivar después."</string>
<string name="screen_create_room_private_option_title">"Sala privada (sólo con invitación)"</string> <string name="screen_create_room_private_option_title">"Sala privada (sólo con invitación)"</string>
<string name="screen_create_room_public_option_description">"Los mensajes no están cifrados y cualquiera puede leerlos. Puedes activar la encriptación más adelante."</string> <string name="screen_create_room_public_option_description">"Los mensajes no están cifrados y cualquiera puede leerlos. Puedes activar la encriptación más adelante."</string>
<string name="screen_create_room_public_option_title">"Sala pública (cualquiera)"</string>
<string name="screen_create_room_room_name_label">"Nombre de la sala"</string> <string name="screen_create_room_room_name_label">"Nombre de la sala"</string>
<string name="screen_create_room_title">"Crear una sala"</string> <string name="screen_create_room_title">"Crear una sala"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string> <string name="screen_create_room_topic_label">"Tema (opcional)"</string>

View file

@ -6,7 +6,7 @@
<string name="screen_create_room_private_option_description">"پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند."</string> <string name="screen_create_room_private_option_description">"پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند."</string>
<string name="screen_create_room_private_option_title">"اتاق خصوصی (فقط دعوت)"</string> <string name="screen_create_room_private_option_title">"اتاق خصوصی (فقط دعوت)"</string>
<string name="screen_create_room_public_option_description">"پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید."</string> <string name="screen_create_room_public_option_description">"پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید."</string>
<string name="screen_create_room_public_option_title">"اتاق عمومی (هرکسی)"</string> <string name="screen_create_room_public_option_title">"اتاق عمومی"</string>
<string name="screen_create_room_room_name_label">"نام اتاق"</string> <string name="screen_create_room_room_name_label">"نام اتاق"</string>
<string name="screen_create_room_title">"ایجاد اتاق"</string> <string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string> <string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>

View file

@ -14,7 +14,7 @@ Ezt bármikor módosíthatja a szobabeállításokban."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Csatlakozás kérése"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
<string name="screen_create_room_room_address_section_footer">"Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."</string> <string name="screen_create_room_room_address_section_footer">"Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."</string>
<string name="screen_create_room_room_address_section_title">"Szoba címe"</string> <string name="screen_create_room_room_address_section_title">"A szoba címe"</string>
<string name="screen_create_room_room_name_label">"Szoba neve"</string> <string name="screen_create_room_room_name_label">"Szoba neve"</string>
<string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string> <string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string>
<string name="screen_create_room_title">"Szoba létrehozása"</string> <string name="screen_create_room_title">"Szoba létrehozása"</string>

View file

@ -14,7 +14,6 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string> <string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
<string name="screen_create_room_room_name_label">"Nama ruangan"</string> <string name="screen_create_room_room_name_label">"Nama ruangan"</string>
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string> <string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
<string name="screen_create_room_title">"Buat ruangan"</string> <string name="screen_create_room_title">"Buat ruangan"</string>

View file

@ -7,7 +7,6 @@
<string name="screen_create_room_private_option_title">"კერძო ოთახი"</string> <string name="screen_create_room_private_option_title">"კერძო ოთახი"</string>
<string name="screen_create_room_public_option_description">"ყველას ამ ოთახის მოძებნა შეუძლია. <string name="screen_create_room_public_option_description">"ყველას ამ ოთახის მოძებნა შეუძლია.
თქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში."</string> თქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში."</string>
<string name="screen_create_room_public_option_title">"საჯარო ოთახი"</string>
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string> <string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
<string name="screen_create_room_title">"ოთახის შექმნა"</string> <string name="screen_create_room_title">"ოთახის შექმნა"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string> <string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>

View file

@ -7,7 +7,6 @@
<string name="screen_create_room_private_option_title">"Sala privativa (somente por convite)"</string> <string name="screen_create_room_private_option_title">"Sala privativa (somente por convite)"</string>
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala. <string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Você pode mudar isso a qualquer momento nas configurações da sala."</string> Você pode mudar isso a qualquer momento nas configurações da sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública (qualquer pessoa)"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string> <string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string> <string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string> <string name="screen_create_room_topic_label">"Tópico (opcional)"</string>

View file

@ -3,7 +3,7 @@
<string name="screen_create_room_action_create_room">"Nova sala"</string> <string name="screen_create_room_action_create_room">"Nova sala"</string>
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string> <string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string> <string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
<string name="screen_create_room_private_option_description">"Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são encriptadas ponta a ponta."</string> <string name="screen_create_room_private_option_description">"Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são cifradas ponta-a-ponta."</string>
<string name="screen_create_room_private_option_title">"Sala privada"</string> <string name="screen_create_room_private_option_title">"Sala privada"</string>
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala. <string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Pode alterar esta opção nas definições da sala."</string> Pode alterar esta opção nas definições da sala."</string>

View file

@ -14,7 +14,6 @@ Puteți modifica acest lucru oricând în setări."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte solicitarea"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte solicitarea"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Cereți să vă alăturați"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Cereți să vă alăturați"</string>
<string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string> <string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string>
<string name="screen_create_room_room_address_section_title">"Adresa camerei"</string>
<string name="screen_create_room_room_name_label">"Numele camerei"</string> <string name="screen_create_room_room_name_label">"Numele camerei"</string>
<string name="screen_create_room_title">"Creați o cameră"</string> <string name="screen_create_room_title">"Creați o cameră"</string>
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string> <string name="screen_create_room_topic_label">"Subiect (opțional)"</string>

View file

@ -13,7 +13,9 @@ Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_room_access_section_header">"Rumsåtkomst"</string> <string name="screen_create_room_room_access_section_header">"Rumsåtkomst"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string>
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
<string name="screen_create_room_room_name_label">"Rumsnamn"</string> <string name="screen_create_room_room_name_label">"Rumsnamn"</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_title">"Skapa ett rum"</string> <string name="screen_create_room_title">"Skapa ett rum"</string>
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string> <string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
<string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string> <string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string>

View file

@ -0,0 +1,23 @@
<?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">"Yeni oda"</string>
<string name="screen_create_room_add_people_title">"İnsanları davet et"</string>
<string name="screen_create_room_error_creating_room">"Oda oluşturulurken bir hata oluştu"</string>
<string name="screen_create_room_private_option_description">"Bu odaya yalnızca davet edilen kişiler erişebilir. Tüm mesajlar uçtan uca şifrelenir."</string>
<string name="screen_create_room_private_option_title">"Özel oda"</string>
<string name="screen_create_room_public_option_description">"Bu odayı herkes bulabilir.
Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."</string>
<string name="screen_create_room_public_option_title">"Herkese açık oda"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Bu odaya herkes katılabilir"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Herkes"</string>
<string name="screen_create_room_room_access_section_header">"Oda Erişimi"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Herkes odaya katılmayı isteyebilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekecektir"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Katılmak için sor"</string>
<string name="screen_create_room_room_address_section_footer">"Bu odanın genel oda dizininde görünür olması için bir oda adresine ihtiyacınız olacaktır."</string>
<string name="screen_create_room_room_address_section_title">"Oda adresi"</string>
<string name="screen_create_room_room_name_label">"Oda adı"</string>
<string name="screen_create_room_room_visibility_section_title">"Oda görünürlüğü"</string>
<string name="screen_create_room_title">"Bir oda oluştur"</string>
<string name="screen_create_room_topic_label">"Konu (isteğe bağlı)"</string>
<string name="screen_start_chat_error_starting_chat">"Sohbet başlatmaya çalışırken bir hata oluştu"</string>
</resources>

View file

@ -7,7 +7,7 @@
<string name="screen_create_room_private_option_title">"Приватна кімната (тільки за запрошенням)"</string> <string name="screen_create_room_private_option_title">"Приватна кімната (тільки за запрошенням)"</string>
<string name="screen_create_room_public_option_description">"Будь-хто може знайти цю кімнату. <string name="screen_create_room_public_option_description">"Будь-хто може знайти цю кімнату.
Ви можете змінити це в будь-який час у налаштуваннях кімнати."</string> Ви можете змінити це в будь-який час у налаштуваннях кімнати."</string>
<string name="screen_create_room_public_option_title">"Публічна кімната"</string> <string name="screen_create_room_public_option_title">"Загальнодоступна кімната"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Будь-хто може приєднатися до цієї кімнати"</string> <string name="screen_create_room_room_access_section_anyone_option_description">"Будь-хто може приєднатися до цієї кімнати"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Кожний"</string> <string name="screen_create_room_room_access_section_anyone_option_title">"Кожний"</string>
<string name="screen_create_room_room_access_section_header">"Доступ до кімнати"</string> <string name="screen_create_room_room_access_section_header">"Доступ до кімнати"</string>

View file

@ -6,7 +6,6 @@
<string name="screen_create_room_private_option_description">"Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni ochirib bolmaydi."</string> <string name="screen_create_room_private_option_description">"Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni ochirib bolmaydi."</string>
<string name="screen_create_room_private_option_title">"Shaxsiy xona (faqat taklif)"</string> <string name="screen_create_room_private_option_title">"Shaxsiy xona (faqat taklif)"</string>
<string name="screen_create_room_public_option_description">"Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."</string> <string name="screen_create_room_public_option_description">"Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin."</string>
<string name="screen_create_room_public_option_title">"Jamoat xonasi (har kim)"</string>
<string name="screen_create_room_room_name_label">"Xona nomi"</string> <string name="screen_create_room_room_name_label">"Xona nomi"</string>
<string name="screen_create_room_title">"Xonani yaratish"</string> <string name="screen_create_room_title">"Xonani yaratish"</string>
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string> <string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>

View file

@ -7,7 +7,7 @@
<string name="screen_create_room_private_option_title">"私密聊天室"</string> <string name="screen_create_room_private_option_title">"私密聊天室"</string>
<string name="screen_create_room_public_option_description">"任何人都可以找到此聊天室。 <string name="screen_create_room_public_option_description">"任何人都可以找到此聊天室。
您隨時都可以在聊天室設定中變更此設定。"</string> 您隨時都可以在聊天室設定中變更此設定。"</string>
<string name="screen_create_room_public_option_title">"公開聊天室"</string> <string name="screen_create_room_public_option_title">"公開聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此聊天室"</string> <string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string> <string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string>
<string name="screen_create_room_room_access_section_header">"聊天室存取權"</string> <string name="screen_create_room_room_access_section_header">"聊天室存取權"</string>

View file

@ -7,7 +7,7 @@
<string name="screen_create_room_private_option_title">"私有聊天室"</string> <string name="screen_create_room_private_option_title">"私有聊天室"</string>
<string name="screen_create_room_public_option_description">"任何人都能找到此聊天室。 <string name="screen_create_room_public_option_description">"任何人都能找到此聊天室。
你可以随时在聊天室设置中更改。"</string> 你可以随时在聊天室设置中更改。"</string>
<string name="screen_create_room_public_option_title">"公聊天室"</string> <string name="screen_create_room_public_option_title">"公聊天室"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此房间"</string> <string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此房间"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string> <string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string>
<string name="screen_create_room_room_access_section_header">"房间访问权限"</string> <string name="screen_create_room_room_access_section_header">"房间访问权限"</string>

View file

@ -10,5 +10,5 @@
<string name="screen_deactivate_account_list_item_2">"Vous retirer de tous les salons et toutes les discussions."</string> <string name="screen_deactivate_account_list_item_2">"Vous retirer de tous les salons et toutes les discussions."</string>
<string name="screen_deactivate_account_list_item_3">"Supprimer les informations de votre compte du serveur didentité."</string> <string name="screen_deactivate_account_list_item_3">"Supprimer les informations de votre compte du serveur didentité."</string>
<string name="screen_deactivate_account_list_item_4">"Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés."</string> <string name="screen_deactivate_account_list_item_4">"Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés."</string>
<string name="screen_deactivate_account_title">"Désactiver votre compte"</string> <string name="screen_deactivate_account_title">"Désactiver le compte"</string>
</resources> </resources>

View file

@ -10,5 +10,5 @@
<string name="screen_deactivate_account_list_item_2">"Ti rimuove da tutte le stanze di chat."</string> <string name="screen_deactivate_account_list_item_2">"Ti rimuove da tutte le stanze di chat."</string>
<string name="screen_deactivate_account_list_item_3">"Elimina le informazioni del tuo account dal nostro server di identità."</string> <string name="screen_deactivate_account_list_item_3">"Elimina le informazioni del tuo account dal nostro server di identità."</string>
<string name="screen_deactivate_account_list_item_4">"I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli."</string> <string name="screen_deactivate_account_list_item_4">"I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli."</string>
<string name="screen_deactivate_account_title">"Disattivazione dell\'account"</string> <string name="screen_deactivate_account_title">"Disattiva account"</string>
</resources> </resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Lütfen hesabınızı devre dışı bırakmak istediğinizi onaylayın. Bu işlem geri alınamaz."</string>
<string name="screen_deactivate_account_delete_all_messages">"Tüm mesajlarımı sil"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Uyarı: Gelecekteki kullanıcılar eksik konuşmalar görebilir."</string>
<string name="screen_deactivate_account_description">"Hesabınızı devre dışı bırakmak %1$s, şunları yapacaktır:"</string>
<string name="screen_deactivate_account_description_bold_part">"geri alınamaz"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s (tekrar giriş yapamazsınız ve kimliğiniz yeniden kullanılamaz)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Kalıcı olarak devre dışı bırak"</string>
<string name="screen_deactivate_account_list_item_2">"Sizi tüm sohbet odalarından çıkarmak."</string>
<string name="screen_deactivate_account_list_item_3">"Hesap bilgileriniz kimlik sunucumuzdan silinecek."</string>
<string name="screen_deactivate_account_list_item_4">"Mesajlarınız kayıtlı kullanıcılar tarafından görülmeye devam eder, ancak silmeyi seçerseniz yeni veya kayıtlı olmayan kullanıcılar tarafından görüntülenemeyecek."</string>
<string name="screen_deactivate_account_title">"Hesabı devre dışı bırak"</string>
</resources>

View file

@ -10,5 +10,5 @@
<string name="screen_deactivate_account_list_item_2">"Видалити вас з усіх чатів."</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_3">"Видаліть інформацію свого облікового запису з нашого сервера ідентифікації."</string>
<string name="screen_deactivate_account_list_item_4">"Ваші повідомлення залишатимуться видимими для зареєстрованих користувачів, але недоступними для нових або незареєстрованих користувачів, якщо ви вирішите їх видалити."</string> <string name="screen_deactivate_account_list_item_4">"Ваші повідомлення залишатимуться видимими для зареєстрованих користувачів, але недоступними для нових або незареєстрованих користувачів, якщо ви вирішите їх видалити."</string>
<string name="screen_deactivate_account_title">"Відключити обліковий запис"</string> <string name="screen_deactivate_account_title">"Деактивувати обліковий запис"</string>
</resources> </resources>

View file

@ -13,6 +13,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.compound)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
} }

View file

@ -7,9 +7,13 @@
package io.element.android.features.enterprise.api package io.element.android.features.enterprise.api
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
fun semanticColorsLight(): SemanticColors
fun semanticColorsDark(): SemanticColors
} }

View file

@ -17,6 +17,7 @@ android {
setupAnvil() setupAnvil()
dependencies { dependencies {
implementation(libs.compound)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
api(projects.features.enterprise.api) api(projects.features.enterprise.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)

View file

@ -8,6 +8,9 @@
package io.element.android.features.enterprise.impl package io.element.android.features.enterprise.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -18,4 +21,8 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override val isEnterpriseBuild = false override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
} }

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Ayarlarınızı daha sonra değiştirebilirsiniz."</string>
<string name="screen_notification_optin_title">"Bildirimlere izin verin ve hiçbir mesajı kaçırmayın"</string>
<string name="screen_welcome_bullet_1">"Çağrılar, anketler, arama ve daha fazlası bu yıl içinde eklenecek."</string>
<string name="screen_welcome_bullet_2">"Şifrelenmiş odalar için mesaj geçmişi henüz mevcut değil."</string>
<string name="screen_welcome_bullet_3">"Düşüncelerinizi bizimle paylaşmanızı çok isteriz. Ayarlar sayfasından düşüncelerinizi bize iletin."</string>
<string name="screen_welcome_button">"Hadi başlayalım!"</string>
<string name="screen_welcome_subtitle">"İşte bilmeniz gerekenler:"</string>
<string name="screen_welcome_title">"%1$s hoş geldiniz!"</string>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"%1$s katılma davetini reddetmek istediğinizden emin misiniz?"</string>
<string name="screen_invites_decline_chat_title">"Daveti reddet"</string>
<string name="screen_invites_decline_direct_chat_message">"%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?"</string>
<string name="screen_invites_decline_direct_chat_title">"Sohbeti reddet"</string>
<string name="screen_invites_empty_list">"Davet Yok"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) sizi davet etti"</string>
</resources>

View file

@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakePendingRoom import io.element.android.libraries.matrix.test.room.FakeRoomPreview
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
@ -77,9 +77,11 @@ class AcceptDeclineInvitePresenterTest {
val declineInviteFailure = lambdaRecorder { -> val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room")) Result.failure<Unit>(RuntimeException("Failed to leave room"))
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient(
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteFailure) getRoomPreviewResult = { _, _ ->
} Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client) val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test { presenter.test {
val inviteData = anInviteData() val inviteData = anInviteData()
@ -120,9 +122,11 @@ class AcceptDeclineInvitePresenterTest {
val declineInviteSuccess = lambdaRecorder { -> val declineInviteSuccess = lambdaRecorder { ->
Result.success(Unit) Result.success(Unit)
} }
val client = FakeMatrixClient().apply { val client = FakeMatrixClient(
getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteSuccess) getRoomPreviewResult = { _, _ ->
} Result.success(FakeRoomPreview(declineInviteResult = declineInviteSuccess))
}
)
val presenter = createAcceptDeclineInvitePresenter( val presenter = createAcceptDeclineInvitePresenter(
client = client, client = client,
notificationCleaner = fakeNotificationCleaner, notificationCleaner = fakeNotificationCleaner,

View file

@ -9,8 +9,10 @@ package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents { sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents data object RetryFetchingContent : JoinRoomEvents
data object DismissErrorAndHideContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents data object JoinRoom : JoinRoomEvents
data object KnockRoom : JoinRoomEvents data object KnockRoom : JoinRoomEvents
data object ForgetRoom : JoinRoomEvents
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
data class UpdateKnockMessage(val message: String) : JoinRoomEvents data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents data object ClearActionStates : JoinRoomEvents

View file

@ -43,6 +43,7 @@ class JoinRoomNode @AssistedInject constructor(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onJoinSuccess = ::navigateUp, onJoinSuccess = ::navigateUp,
onForgetSuccess = ::navigateUp,
onCancelKnockSuccess = {}, onCancelKnockSuccess = {},
onKnockSuccess = {}, onKnockSuccess = {},
modifier = modifier modifier = modifier

View file

@ -26,22 +26,28 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.joinroom.impl.di.CancelKnockRoom import io.element.android.features.joinroom.impl.di.CancelKnockRoom
import io.element.android.features.joinroom.impl.di.ForgetRoom
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.getRoomInfoFlow import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -58,6 +64,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private val joinRoom: JoinRoom, private val joinRoom: JoinRoom,
private val knockRoom: KnockRoom, private val knockRoom: KnockRoom,
private val cancelKnockRoom: CancelKnockRoom, private val cancelKnockRoom: CancelKnockRoom,
private val forgetRoom: ForgetRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
@ -79,31 +86,59 @@ class JoinRoomPresenter @AssistedInject constructor(
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var knockMessage by rememberSaveable { mutableStateOf("") } var knockMessage by rememberSaveable { mutableStateOf("") }
var isDismissingContent by remember { mutableStateOf(false) }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
initialValue = ContentState.Loading(roomIdOrAlias), initialValue = ContentState.Loading,
key1 = roomInfo, key1 = roomInfo,
key2 = retryCount, key2 = retryCount,
key3 = isDismissingContent,
) { ) {
when { when {
isDismissingContent -> value = ContentState.Dismissing
roomInfo.isPresent -> { roomInfo.isPresent -> {
value = roomInfo.get().toContentState() val (sender, reason) = when (roomInfo.get().currentUserMembership) {
CurrentUserMembership.BANNED -> {
// Workaround to get info about the sender for banned rooms
// TODO re-do this once we have a better API in the SDK
val preview = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
val membershipDetalis = preview.getOrNull()?.membershipDetails()?.getOrNull()
membershipDetalis?.senderMember to membershipDetalis?.currentUserMember?.membershipChangeReason
}
CurrentUserMembership.INVITED -> {
roomInfo.get().inviter to null
}
else -> null to null
}
value = roomInfo.get().toContentState(sender, reason)
} }
roomDescription.isPresent -> { roomDescription.isPresent -> {
value = roomDescription.get().toContentState() value = roomDescription.get().toContentState()
} }
else -> { else -> {
value = ContentState.Loading(roomIdOrAlias) value = ContentState.Loading
val result = matrixClient.getRoomPreviewInfo(roomIdOrAlias, serverNames) val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
value = result.fold( value = result.fold(
onSuccess = { previewInfo -> onSuccess = { preview ->
previewInfo.toContentState() val membershipInfo = when (preview.info.membership) {
CurrentUserMembership.INVITED,
CurrentUserMembership.BANNED,
CurrentUserMembership.KNOCKED -> {
preview.membershipDetails().getOrNull()
}
else -> null
}
preview.info.toContentState(
senderMember = membershipInfo?.senderMember,
reason = membershipInfo?.currentUserMember?.membershipChangeReason,
)
}, },
onFailure = { throwable -> onFailure = { throwable ->
if (throwable.message?.contains("403") == true) { if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
ContentState.UnknownRoom(roomIdOrAlias) ContentState.UnknownRoom
} else { } else {
ContentState.Failure(roomIdOrAlias, throwable) ContentState.Failure(throwable)
} }
} }
) )
@ -136,18 +171,25 @@ class JoinRoomPresenter @AssistedInject constructor(
knockAction.value = AsyncAction.Uninitialized knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized cancelKnockAction.value = AsyncAction.Uninitialized
forgetRoomAction.value = AsyncAction.Uninitialized
} }
is JoinRoomEvents.UpdateKnockMessage -> { is JoinRoomEvents.UpdateKnockMessage -> {
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH) knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
} }
JoinRoomEvents.DismissErrorAndHideContent -> {
isDismissingContent = true
}
JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
} }
} }
return JoinRoomState( return JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction.value, joinAction = joinAction.value,
knockAction = knockAction.value, knockAction = knockAction.value,
forgetAction = forgetRoomAction.value,
cancelKnockAction = cancelKnockAction.value, cancelKnockAction = cancelKnockAction.value,
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
knockMessage = knockMessage, knockMessage = knockMessage,
@ -161,7 +203,13 @@ class JoinRoomPresenter @AssistedInject constructor(
roomIdOrAlias = roomIdOrAlias, roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames, serverNames = serverNames,
trigger = trigger trigger = trigger
) ).mapFailure {
if (it is ClientException.MatrixApi && it.kind == ErrorKind.Forbidden) {
JoinRoomFailures.UnauthorizedJoin
} else {
it
}
}
} }
} }
@ -180,9 +228,15 @@ class JoinRoomPresenter @AssistedInject constructor(
} }
} }
} }
private fun CoroutineScope.forgetRoom(forgetAction: MutableState<AsyncAction<Unit>>) = launch {
forgetAction.runUpdatingState {
forgetRoom.invoke(roomId)
}
}
} }
private fun RoomPreviewInfo.toContentState(): ContentState { private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
roomId = roomId, roomId = roomId,
name = name, name = name,
@ -192,12 +246,11 @@ private fun RoomPreviewInfo.toContentState(): ContentState {
isDm = false, isDm = false,
roomType = roomType, roomType = roomType,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when { joinAuthorisationStatus = when (membership) {
// Note when isInvited, roomInfo will be used, so if this happen, it will be temporary. CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(senderMember?.toInviteSender())
isInvited -> JoinAuthorisationStatus.IsInvited(null) CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
canKnock -> JoinAuthorisationStatus.CanKnock CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
isPublic -> JoinAuthorisationStatus.CanJoin else -> joinRule.toJoinAuthorisationStatus()
else -> JoinAuthorisationStatus.Unknown
} }
) )
} }
@ -222,27 +275,42 @@ internal fun RoomDescription.toContentState(): ContentState {
} }
@VisibleForTesting @VisibleForTesting
internal fun MatrixRoomInfo.toContentState(): ContentState { internal fun MatrixRoomInfo.toContentState(membershipSender: RoomMember?, reason: String?): ContentState {
return ContentState.Loaded( return ContentState.Loaded(
roomId = id, roomId = id,
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = activeMembersCount, numberOfMembers = joinedMembersCount,
isDm = isDm, isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room, roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
joinAuthorisationStatus = when { joinAuthorisationStatus = when (currentUserMembership) {
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
inviteSender = inviter?.toInviteSender() inviteSender = membershipSender?.toInviteSender()
) )
currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
isPublic -> JoinAuthorisationStatus.CanJoin banSender = membershipSender?.toInviteSender(),
else -> JoinAuthorisationStatus.Unknown reason = reason,
)
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
else -> joinRule.toJoinAuthorisationStatus()
} }
) )
} }
private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
return when (this) {
JoinRule.Knock,
is JoinRule.KnockRestricted -> JoinAuthorisationStatus.CanKnock
JoinRule.Invite,
JoinRule.Private -> JoinAuthorisationStatus.NeedInvite
is JoinRule.Restricted -> JoinAuthorisationStatus.Restricted
JoinRule.Public -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown
}
}
@VisibleForTesting @VisibleForTesting
internal fun ContentState.toInviteData(): InviteData? { internal fun ContentState.toInviteData(): InviteData? {
return when (this) { return when (this) {

View file

@ -22,30 +22,49 @@ internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
@Immutable @Immutable
data class JoinRoomState( data class JoinRoomState(
val roomIdOrAlias: RoomIdOrAlias,
val contentState: ContentState, val contentState: ContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val joinAction: AsyncAction<Unit>, val joinAction: AsyncAction<Unit>,
val knockAction: AsyncAction<Unit>, val knockAction: AsyncAction<Unit>,
val forgetAction: AsyncAction<Unit>,
val cancelKnockAction: AsyncAction<Unit>, val cancelKnockAction: AsyncAction<Unit>,
val applicationName: String, private val applicationName: String,
val knockMessage: String, val knockMessage: String,
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
val joinAuthorisationStatus = when (contentState) { val joinAuthorisationStatus = when (contentState) {
// Use the join authorisation status from the loaded content state is ContentState.Loaded -> {
is ContentState.Loaded -> contentState.joinAuthorisationStatus when {
// Assume that if the room is unknown, the user can join it contentState.roomType == RoomType.Space -> {
is ContentState.UnknownRoom -> JoinAuthorisationStatus.CanJoin JoinAuthorisationStatus.IsSpace(applicationName)
// Otherwise assume that the user can't join the room }
else -> JoinAuthorisationStatus.Unknown isJoinActionUnauthorized -> {
JoinAuthorisationStatus.Unauthorized
}
else -> {
contentState.joinAuthorisationStatus
}
}
}
is ContentState.UnknownRoom -> {
if (isJoinActionUnauthorized) {
JoinAuthorisationStatus.Unauthorized
} else {
JoinAuthorisationStatus.Unknown
}
}
else -> JoinAuthorisationStatus.None
} }
} }
@Immutable @Immutable
sealed interface ContentState { sealed interface ContentState {
data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState data object Dismissing : ContentState
data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState data object Loading : ContentState
data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState data class Failure(val error: Throwable) : ContentState
data object UnknownRoom : ContentState
data class Loaded( data class Loaded(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
@ -71,9 +90,19 @@ sealed interface ContentState {
} }
sealed interface JoinAuthorisationStatus { sealed interface JoinAuthorisationStatus {
data object None : JoinAuthorisationStatus
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
data object IsKnocked : JoinAuthorisationStatus data object IsKnocked : JoinAuthorisationStatus
data object CanKnock : JoinAuthorisationStatus data object CanKnock : JoinAuthorisationStatus
data object CanJoin : JoinAuthorisationStatus data object CanJoin : JoinAuthorisationStatus
data object NeedInvite : JoinAuthorisationStatus
data object Restricted : JoinAuthorisationStatus
data object Unknown : JoinAuthorisationStatus data object Unknown : JoinAuthorisationStatus
data object Unauthorized : JoinAuthorisationStatus
}
sealed class JoinRoomFailures : Exception() {
data object UnauthorizedJoin : JoinRoomFailures()
} }

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
@ -25,10 +26,10 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState> override val values: Sequence<JoinRoomState>
get() = sequenceOf( get() = sequenceOf(
aJoinRoomState( aJoinRoomState(
contentState = aLoadingContentState() contentState = ContentState.Loading
), ),
aJoinRoomState( aJoinRoomState(
contentState = anUnknownContentState() contentState = ContentState.UnknownRoom
), ),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState( contentState = aLoadedContentState(
@ -40,6 +41,14 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
), ),
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
joinAction = AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin)
),
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong"))
),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)) contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
), ),
@ -52,9 +61,6 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
aJoinRoomState( aJoinRoomState(
contentState = aFailureContentState() contentState = aFailureContentState()
), ),
aJoinRoomState(
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
),
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState( contentState = aLoadedContentState(
roomId = RoomId("!aSpaceId:domain"), roomId = RoomId("!aSpaceId:domain"),
@ -98,23 +104,42 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
name = "A knocked Room", name = "A knocked Room",
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
) )
) ),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A private room",
joinAuthorisationStatus = JoinAuthorisationStatus.NeedInvite
)
),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A banned room",
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
banSender = InviteSender(
userId = UserId("@alice:domain"),
displayName = "Alice",
avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender),
membershipChangeReason = "spamming"
),
reason = "spamming",
),
)
),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A restricted room",
joinAuthorisationStatus = JoinAuthorisationStatus.Restricted,
)
),
) )
} }
fun aFailureContentState( fun aFailureContentState(): ContentState {
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias()
): ContentState {
return ContentState.Failure( return ContentState.Failure(
roomIdOrAlias = roomIdOrAlias,
error = Exception("Error"), error = Exception("Error"),
) )
} }
fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias())
fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias())
fun aLoadedContentState( fun aLoadedContentState(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = "Element X android", name: String? = "Element X android",
@ -138,19 +163,23 @@ fun aLoadedContentState(
) )
fun aJoinRoomState( fun aJoinRoomState(
roomIdOrAlias: RoomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias(),
contentState: ContentState = aLoadedContentState(), contentState: ContentState = aLoadedContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized, joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized, knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized, cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockMessage: String = "", knockMessage: String = "",
eventSink: (JoinRoomEvents) -> Unit = {} eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState( ) = JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction, joinAction = joinAction,
knockAction = knockAction, knockAction = knockAction,
cancelKnockAction = cancelKnockAction, cancelKnockAction = cancelKnockAction,
forgetAction = forgetAction,
applicationName = "AppName", applicationName = "AppName",
knockMessage = knockMessage, knockMessage = knockMessage,
eventSink = eventSink eventSink = eventSink
@ -160,10 +189,12 @@ internal fun anInviteSender(
userId: UserId = UserId("@bob:domain"), userId: UserId = UserId("@bob:domain"),
displayName: String = "Bob", displayName: String = "Bob",
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender), avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
membershipChangeReason: String? = null,
) = InviteSender( ) = InviteSender(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
avatarData = avatarData, avatarData = avatarData,
membershipChangeReason = membershipChangeReason,
) )
private val A_ROOM_ID = RoomId("!exa:matrix.org") private val A_ROOM_ID = RoomId("!exa:matrix.org")

View file

@ -19,12 +19,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -32,7 +32,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -47,7 +46,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubti
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.LightGradientBackground import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -55,16 +55,17 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.SuperButton import io.element.android.libraries.designsystem.components.button.SuperButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -74,30 +75,33 @@ fun JoinRoomView(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onJoinSuccess: () -> Unit, onJoinSuccess: () -> Unit,
onKnockSuccess: () -> Unit, onKnockSuccess: () -> Unit,
onForgetSuccess: () -> Unit,
onCancelKnockSuccess: () -> Unit, onCancelKnockSuccess: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) { ) {
LightGradientBackground()
HeaderFooterPage( HeaderFooterPage(
containerColor = Color.Transparent, containerColor = Color.Transparent,
paddingValues = PaddingValues(16.dp), paddingValues = PaddingValues(
horizontal = 16.dp,
vertical = 32.dp
),
topBar = { topBar = {
JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick) JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
}, },
content = { content = {
JoinRoomContent( JoinRoomContent(
roomIdOrAlias = state.roomIdOrAlias,
contentState = state.contentState, contentState = state.contentState,
applicationName = state.applicationName,
knockMessage = state.knockMessage, knockMessage = state.knockMessage,
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) }, onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
) )
}, },
footer = { footer = {
JoinRoomFooter( JoinRoomFooter(
state = state, joinAuthorisationStatus = state.joinAuthorisationStatus,
onAcceptInvite = { onAcceptInvite = {
state.eventSink(JoinRoomEvents.AcceptInvite) state.eventSink(JoinRoomEvents.AcceptInvite)
}, },
@ -113,31 +117,55 @@ fun JoinRoomView(
onCancelKnock = { onCancelKnock = {
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true)) state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
}, },
onRetry = { onForgetRoom = {
state.eventSink(JoinRoomEvents.RetryFetchingContent) state.eventSink(JoinRoomEvents.ForgetRoom)
}, },
onGoBack = onBackClick, onGoBack = onBackClick,
) )
} }
) )
} }
if (state.contentState is ContentState.Failure) {
RetryDialog(
title = stringResource(R.string.screen_join_room_loading_alert_title),
content = stringResource(CommonStrings.error_network_or_server_issue),
onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) },
onDismiss = {
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
onBackClick()
}
)
}
// This particular error is shown directly in the footer
if (!state.isJoinActionUnauthorized) {
AsyncActionView(
async = state.joinAction,
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onJoinSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
)
}
AsyncActionView( AsyncActionView(
async = state.joinAction, async = state.knockAction,
onSuccess = { onJoinSuccess() }, errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
) )
AsyncActionView( AsyncActionView(
async = state.knockAction, async = state.forgetAction,
onSuccess = { onKnockSuccess() }, errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onForgetSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
) )
AsyncActionView( AsyncActionView(
async = state.cancelKnockAction, async = state.cancelKnockAction,
onSuccess = { onCancelKnockSuccess() }, onSuccess = { onCancelKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
errorMessage = { errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
stringResource(CommonStrings.error_unknown) errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
},
confirmationDialog = { confirmationDialog = {
ConfirmationDialog( ConfirmationDialog(
content = stringResource(R.string.screen_join_room_cancel_knock_alert_description), content = stringResource(R.string.screen_join_room_cancel_knock_alert_description),
@ -153,13 +181,13 @@ fun JoinRoomView(
@Composable @Composable
private fun JoinRoomFooter( private fun JoinRoomFooter(
state: JoinRoomState, joinAuthorisationStatus: JoinAuthorisationStatus,
onAcceptInvite: () -> Unit, onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit, onDeclineInvite: () -> Unit,
onJoinRoom: () -> Unit, onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit, onKnockRoom: () -> Unit,
onCancelKnock: () -> Unit, onCancelKnock: () -> Unit,
onRetry: () -> Unit, onForgetRoom: () -> Unit,
onGoBack: () -> Unit, onGoBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -168,79 +196,170 @@ private fun JoinRoomFooter(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .padding(top = 8.dp)
) { ) {
if (state.contentState is ContentState.Failure) { when (joinAuthorisationStatus) {
Button( is JoinAuthorisationStatus.IsInvited -> {
text = stringResource(CommonStrings.action_retry), ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
onClick = onRetry,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
Button(
text = stringResource(CommonStrings.action_go_back),
onClick = onGoBack,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
} else {
val joinAuthorisationStatus = state.joinAuthorisationStatus
when (joinAuthorisationStatus) {
is JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = onDeclineInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
}
}
JoinAuthorisationStatus.CanJoin -> {
SuperButton(
onClick = onJoinRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_join_action),
)
}
}
JoinAuthorisationStatus.CanKnock -> {
SuperButton(
onClick = onKnockRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_knock_action),
)
}
}
JoinAuthorisationStatus.IsKnocked -> {
OutlinedButton( OutlinedButton(
text = stringResource(R.string.screen_join_room_cancel_knock_action), text = stringResource(CommonStrings.action_decline),
onClick = onCancelKnock, onClick = onDeclineInvite,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(1f),
size = ButtonSize.Large, size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
) )
} }
JoinAuthorisationStatus.Unknown -> Unit
} }
JoinAuthorisationStatus.CanJoin -> {
SuperButton(
onClick = onJoinRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_join_action),
)
}
}
JoinAuthorisationStatus.CanKnock -> {
SuperButton(
onClick = onKnockRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_knock_action),
)
}
}
JoinAuthorisationStatus.IsKnocked -> {
OutlinedButton(
text = stringResource(R.string.screen_join_room_cancel_knock_action),
onClick = onCancelKnock,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
}
JoinAuthorisationStatus.NeedInvite -> {
Announcement(
title = stringResource(R.string.screen_join_room_invite_required_message),
description = null,
type = AnnouncementType.Informative(isCritical = false),
)
}
is JoinAuthorisationStatus.IsBanned -> JoinBannedFooter(joinAuthorisationStatus, onForgetRoom)
JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom)
JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack)
is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack)
JoinAuthorisationStatus.None -> Unit
} }
} }
} }
@Composable @Composable
private fun JoinRoomContent( private fun JoinUnauthorizedFooter(
contentState: ContentState, onOkClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Announcement(
title = stringResource(R.string.screen_join_room_fail_message),
description = stringResource(R.string.screen_join_room_fail_reason),
type = AnnouncementType.Informative(isCritical = true),
)
Spacer(Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_ok),
onClick = onOkClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun JoinBannedFooter(
status: JoinAuthorisationStatus.IsBanned,
onForgetRoom: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
val banReason = status.reason?.let {
stringResource(R.string.screen_join_room_ban_reason, it.removeSuffix("."))
}
val title = if (status.banSender != null) {
stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName)
} else {
stringResource(R.string.screen_join_room_ban_message)
}
Announcement(
title = title,
description = banReason,
type = AnnouncementType.Informative(isCritical = true),
)
Spacer(Modifier.height(24.dp))
Button(
text = stringResource(R.string.screen_join_room_forget_action),
onClick = onForgetRoom,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
}
}
@Composable
private fun JoinRestrictedFooter(
onJoinRoom: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Announcement(
title = stringResource(R.string.screen_join_room_join_restricted_message),
description = null,
type = AnnouncementType.Informative(),
)
Spacer(Modifier.height(24.dp))
SuperButton(
onClick = onJoinRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_join_action),
)
}
}
}
@Composable
private fun UnsupportedSpaceFooter(
applicationName: String, applicationName: String,
onGoBack: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Announcement(
title = stringResource(R.string.screen_join_room_space_not_supported_title),
description = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
type = AnnouncementType.Informative(),
)
Spacer(Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_ok),
onClick = onGoBack,
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.Large,
)
}
}
@Composable
private fun JoinRoomContent(
roomIdOrAlias: RoomIdOrAlias,
contentState: ContentState,
knockMessage: String, knockMessage: String,
onKnockMessageUpdate: (String) -> Unit, onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -256,67 +375,67 @@ private fun JoinRoomContent(
DefaultLoadedContent( DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState, contentState = contentState,
applicationName = applicationName,
knockMessage = knockMessage, knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate onKnockMessageUpdate = onKnockMessageUpdate
) )
} }
} }
} }
is ContentState.UnknownRoom -> { is ContentState.UnknownRoom -> UnknownRoomContent()
RoomPreviewOrganism( is ContentState.Loading -> IncompleteContent(roomIdOrAlias, isLoading = true)
avatar = { is ContentState.Dismissing -> IncompleteContent(roomIdOrAlias, isLoading = false)
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) is ContentState.Failure -> IncompleteContent(roomIdOrAlias, isLoading = false)
},
title = {
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
},
subtitle = {
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
},
)
}
is ContentState.Loading -> {
RoomPreviewOrganism(
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = {
PlaceholderAtom(width = 200.dp, height = 22.dp)
},
subtitle = {
PlaceholderAtom(width = 140.dp, height = 20.dp)
},
)
}
is ContentState.Failure -> {
RoomPreviewOrganism(
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = {
when (contentState.roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
}
is RoomIdOrAlias.Id -> {
PlaceholderAtom(width = 200.dp, height = 22.dp)
}
}
},
subtitle = {
Text(
text = stringResource(id = CommonStrings.error_unknown),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error,
)
},
)
}
} }
} }
} }
@Composable
private fun UnknownRoomContent(
modifier: Modifier = Modifier
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp))
},
title = {
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
},
subtitle = {
},
)
}
@Composable
private fun IncompleteContent(
roomIdOrAlias: RoomIdOrAlias,
isLoading: Boolean,
modifier: Modifier = Modifier
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = {
when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
RoomPreviewSubtitleAtom(roomIdOrAlias.identifier)
}
is RoomIdOrAlias.Id -> {
PlaceholderAtom(width = 200.dp, height = 22.dp)
}
}
},
subtitle = {
if (isLoading) {
Spacer(Modifier.height(8.dp))
CircularProgressIndicator()
}
},
)
}
@Composable @Composable
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints( BoxWithConstraints(
@ -337,7 +456,6 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
@Composable @Composable
private fun DefaultLoadedContent( private fun DefaultLoadedContent(
contentState: ContentState.Loaded, contentState: ContentState.Loaded,
applicationName: String,
knockMessage: String, knockMessage: String,
onKnockMessageUpdate: (String) -> Unit, onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -374,21 +492,7 @@ private fun DefaultLoadedContent(
InviteSenderView(inviteSender = inviteSender) InviteSenderView(inviteSender = inviteSender)
} }
RoomPreviewDescriptionAtom(contentState.topic ?: "") RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.roomType == RoomType.Space) { if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.screen_join_room_space_not_supported_title),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyLgMedium,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
textAlign = TextAlign.Center,
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
)
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
val supportingText = if (knockMessage.isNotEmpty()) { val supportingText = if (knockMessage.isNotEmpty()) {
"${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH" "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
@ -462,6 +566,7 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
onBackClick = { }, onBackClick = { },
onJoinSuccess = { }, onJoinSuccess = { },
onKnockSuccess = { }, onKnockSuccess = { },
onForgetSuccess = { },
onCancelKnockSuccess = { }, onCancelKnockSuccess = { },
) )
} }

View file

@ -0,0 +1,28 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject
interface ForgetRoom {
suspend operator fun invoke(roomId: RoomId): Result<Unit>
}
@ContributesBinding(SessionScope::class)
class DefaultForgetRoom @Inject constructor(private val client: MatrixClient) : ForgetRoom {
override suspend fun invoke(roomId: RoomId): Result<Unit> {
return client
.getPendingRoom(roomId)
?.forget()
?: Result.failure(IllegalStateException("No pending room found"))
}
}

View file

@ -32,6 +32,7 @@ object JoinRoomModule {
joinRoom: JoinRoom, joinRoom: JoinRoom,
knockRoom: KnockRoom, knockRoom: KnockRoom,
cancelKnockRoom: CancelKnockRoom, cancelKnockRoom: CancelKnockRoom,
forgetRoom: ForgetRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
buildMeta: BuildMeta, buildMeta: BuildMeta,
): JoinRoomPresenter.Factory { ): JoinRoomPresenter.Factory {
@ -52,6 +53,7 @@ object JoinRoomModule {
matrixClient = client, matrixClient = client,
joinRoom = joinRoom, joinRoom = joinRoom,
knockRoom = knockRoom, knockRoom = knockRoom,
forgetRoom = forgetRoom,
cancelKnockRoom = cancelKnockRoom, cancelKnockRoom = cancelKnockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
buildMeta = buildMeta, buildMeta = buildMeta,

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ano, zrušit"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Ano, zrušit"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Opravdu chcete zrušit svou žádost o vstup do této místnosti?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Opravdu chcete zrušit svou žádost o vstup do této místnosti?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Zrušit žádost o vstup"</string> <string name="screen_join_room_cancel_knock_alert_title">"Zrušit žádost o vstup"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ano, odmítnout a zablokovat"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Opravdu chcete odmítnout pozvánku do této místnosti? Tím také zabráníte tomu, aby vás %1$s kontaktoval(a) nebo pozval(a) do místností."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Odmítnout pozvání a zablokovat"</string>
<string name="screen_join_room_decline_and_block_button_title">"Odmítnout a zablokovat"</string>
<string name="screen_join_room_fail_message">"Vstup do místnosti se nezdařil."</string> <string name="screen_join_room_fail_message">"Vstup do místnosti se nezdařil."</string>
<string name="screen_join_room_fail_reason">"Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru."</string> <string name="screen_join_room_fail_reason">"Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru."</string>
<string name="screen_join_room_forget_action">"Zapomenout na tuto místnost"</string> <string name="screen_join_room_forget_action">"Zapomenout na tuto místnost"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Zpráva (nepovinné)"</string> <string name="screen_join_room_knock_message_description">"Zpráva (nepovinné)"</string>
<string name="screen_join_room_knock_sent_description">"Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."</string> <string name="screen_join_room_knock_sent_description">"Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."</string>
<string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</string> <string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</string>
<string name="screen_join_room_loading_alert_message">"Náhled místnosti se nám nepodařilo zobrazit. To může být způsobeno problémy se sítí nebo serverem."</string>
<string name="screen_join_room_loading_alert_title">"Náhled této místnosti jsme nemohli zobrazit"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s zatím nepodporuje prostory. Prostory můžete používat na webu."</string> <string name="screen_join_room_space_not_supported_description">"%1$s zatím nepodporuje prostory. Prostory můžete používat na webu."</string>
<string name="screen_join_room_space_not_supported_title">"Prostory zatím nejsou podporovány"</string> <string name="screen_join_room_space_not_supported_title">"Prostory zatím nejsou podporovány"</string>
<string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string> <string name="screen_join_room_subtitle_knock">"Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string> <string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
<string name="screen_join_room_fail_message">"Der Beitritt zum Chatroom schlug fehl."</string> <string name="screen_join_room_fail_message">"Der Beitritt zum Chatroom schlug fehl."</string>
<string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es kann zu Zugangsbeschränkungen auf Spaceebene kommen."</string> <string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es kann zu Zugangsbeschränkungen auf Spaceebene kommen."</string>
<string name="screen_join_room_forget_action">"Vergessen Sie diesen Raum"</string> <string name="screen_join_room_forget_action">"Vergessen Sie diesen Raum"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string> <string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string>
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."</string> <string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."</string>
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string> <string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
<string name="screen_join_room_loading_alert_message">"Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."</string>
<string name="screen_join_room_loading_alert_title">"Wir konnten diese Chatroomvorschau nicht anzeigen"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string> <string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string> <string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string> <string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Jah, tühista"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Jah, tühista"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Tühista liitumispalve"</string> <string name="screen_join_room_cancel_knock_alert_title">"Tühista liitumispalve"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Jah, keeldu ja blokeeri"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Kas sa oled kindel, et soovid keelduda kutsest sellesse jututuppa? Samaga kaob kasutajal %1$s võimalus sinuga suhelda ja saata sulle jututubade kutseid."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Keeldu kutsest ja blokeeri"</string>
<string name="screen_join_room_decline_and_block_button_title">"Keeldu ja blokeeri"</string>
<string name="screen_join_room_fail_message">"Jututoaga liitumine ei õnnestunud."</string> <string name="screen_join_room_fail_message">"Jututoaga liitumine ei õnnestunud."</string>
<string name="screen_join_room_fail_reason">"Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."</string> <string name="screen_join_room_fail_reason">"Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."</string>
<string name="screen_join_room_forget_action">"Unusta see jututuba"</string> <string name="screen_join_room_forget_action">"Unusta see jututuba"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Selgitus (kui soovid lisada)"</string> <string name="screen_join_room_knock_message_description">"Selgitus (kui soovid lisada)"</string>
<string name="screen_join_room_knock_sent_description">"Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."</string> <string name="screen_join_room_knock_sent_description">"Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."</string>
<string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string> <string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string>
<string name="screen_join_room_loading_alert_message">"Me ei saanud jututoa eelvaadet näidata. See võib olla põhjustatud võrguühenduse või serveri vigadest."</string>
<string name="screen_join_room_loading_alert_title">"Meil ei õnnestunud selle jututoa eelvaadet kuvada"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s veel ei toeta kogukondadega liitumise ja kasutamise võimalust. Vajadusel saad seda teha veebiliidese vahendusel."</string> <string name="screen_join_room_space_not_supported_description">"%1$s veel ei toeta kogukondadega liitumise ja kasutamise võimalust. Vajadusel saad seda teha veebiliidese vahendusel."</string>
<string name="screen_join_room_space_not_supported_title">"Kogukonnad pole veel toetatud"</string> <string name="screen_join_room_space_not_supported_title">"Kogukonnad pole veel toetatud"</string>
<string name="screen_join_room_subtitle_knock">"Klõpsi allolevat nuppu ja jututoa haldaja saab asjakohase teate. Sa saad liituda, kui haldaja sinu soovi heaks kiidab."</string> <string name="screen_join_room_subtitle_knock">"Klõpsi allolevat nuppu ja jututoa haldaja saab asjakohase teate. Sa saad liituda, kui haldaja sinu soovi heaks kiidab."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Oui, annuler"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Oui, annuler"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Êtes-vous sûr de vouloir annuler votre demande daccès à ce salon ?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Êtes-vous sûr de vouloir annuler votre demande daccès à ce salon ?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Annuler la demande dadhésion"</string> <string name="screen_join_room_cancel_knock_alert_title">"Annuler la demande dadhésion"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Oui, refuser et bloquer"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Êtes-vous sûr de vouloir refuser linvitation à rejoindre ce salon ? Cela empêchera également %1$s de vous contacter ou de vous inviter dans les salons."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Refuser linvitation et bloquer"</string>
<string name="screen_join_room_decline_and_block_button_title">"Refuser et bloquer"</string>
<string name="screen_join_room_fail_message">"Rejoindre le salon a échoué."</string> <string name="screen_join_room_fail_message">"Rejoindre le salon a échoué."</string>
<string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions daccès au niveau du Space."</string> <string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions daccès au niveau du Space."</string>
<string name="screen_join_room_forget_action">"Oublier ce salon"</string> <string name="screen_join_room_forget_action">"Oublier ce salon"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Message (facultatif)"</string> <string name="screen_join_room_knock_message_description">"Message (facultatif)"</string>
<string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string> <string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string>
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string> <string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string>
<string name="screen_join_room_loading_alert_message">"Impossible dafficher laperçu du salon. Cela peut être dû à des problèmes de réseau ou de serveur."</string>
<string name="screen_join_room_loading_alert_title">"Impossible dafficher laperçu de ce salon"</string>
<string name="screen_join_room_space_not_supported_description">"Les Spaces ne sont pas encore pris en charge par %1$s . Vous pouvez voir les Spaces sur le Web."</string> <string name="screen_join_room_space_not_supported_description">"Les Spaces ne sont pas encore pris en charge par %1$s . Vous pouvez voir les Spaces sur le Web."</string>
<string name="screen_join_room_space_not_supported_title">"Les Spaces ne sont pas encore pris en charge"</string> <string name="screen_join_room_space_not_supported_title">"Les Spaces ne sont pas encore pris en charge"</string>
<string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string> <string name="screen_join_room_subtitle_knock">"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Biztos, hogy visszavonja a szobához való csatlakozási kérését?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Biztos, hogy visszavonja a szobához való csatlakozási kérését?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Csatlakozási kérés visszavonása"</string> <string name="screen_join_room_cancel_knock_alert_title">"Csatlakozási kérés visszavonása"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Igen, elutasítás és blokkolás"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Meghívó elutasítása és blokkolás"</string>
<string name="screen_join_room_decline_and_block_button_title">"Elutasítás és blokkolás"</string>
<string name="screen_join_room_fail_message">"A szobához való csatlakozás sikertelen."</string> <string name="screen_join_room_fail_message">"A szobához való csatlakozás sikertelen."</string>
<string name="screen_join_room_fail_reason">"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."</string> <string name="screen_join_room_fail_reason">"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."</string>
<string name="screen_join_room_forget_action">"Szoba elfelejtése"</string> <string name="screen_join_room_forget_action">"Szoba elfelejtése"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Üzenet (nem kötelező)"</string> <string name="screen_join_room_knock_message_description">"Üzenet (nem kötelező)"</string>
<string name="screen_join_room_knock_sent_description">"Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."</string> <string name="screen_join_room_knock_sent_description">"Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."</string>
<string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</string> <string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</string>
<string name="screen_join_room_loading_alert_message">"Nem tudtuk megjeleníteni a szoba előnézetét. Ennek az oka hálózati vagy kiszolgálóprobléma is lehet."</string>
<string name="screen_join_room_loading_alert_title">"Nem tudtuk megjeleníteni a szoba előnézetét"</string>
<string name="screen_join_room_space_not_supported_description">"Az %1$s még nem támogatja a tereket. A tereket a weben érheti el."</string> <string name="screen_join_room_space_not_supported_description">"Az %1$s még nem támogatja a tereket. A tereket a weben érheti el."</string>
<string name="screen_join_room_space_not_supported_title">"A terek még nem támogatottak"</string> <string name="screen_join_room_space_not_supported_title">"A terek még nem támogatottak"</string>
<string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string> <string name="screen_join_room_subtitle_knock">"Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez."</string>

View file

@ -1,14 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Zostałeś zbanowany z tego pokoju przez %1$s."</string>
<string name="screen_join_room_ban_message">"Zostałeś zbanowany z tego pokoju"</string>
<string name="screen_join_room_ban_reason">"Powód: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Anuluj prośbę"</string> <string name="screen_join_room_cancel_knock_action">"Anuluj prośbę"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Tak, anuluj"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Tak, anuluj"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Czy na pewno chcesz anulować prośbę o dołączenie do tego pokoju?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Czy na pewno chcesz anulować prośbę o dołączenie do tego pokoju?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Anuluj prośbę o dołączenie"</string> <string name="screen_join_room_cancel_knock_alert_title">"Anuluj prośbę o dołączenie"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Tak, odrzuć i zablokuj"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do tego pokoju? %1$s nie będzie mógł się również z Tobą skontaktować, ani zaprosić Cię do pokoju."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Odrzuć zaproszenie i zablokuj"</string>
<string name="screen_join_room_decline_and_block_button_title">"Odrzuć i zablokuj"</string>
<string name="screen_join_room_fail_message">"Nie udało się dołączyć do pokoju."</string>
<string name="screen_join_room_fail_reason">"Ten pokój wymaga zaproszenia lub jest ograniczony z poziomu przestrzeni."</string>
<string name="screen_join_room_forget_action">"Zapomnij o tym pokoju"</string>
<string name="screen_join_room_invite_required_message">"Potrzebujesz zaproszenia, aby dołączyć do tego pokoju"</string>
<string name="screen_join_room_join_action">"Dołącz do pokoju"</string> <string name="screen_join_room_join_action">"Dołącz do pokoju"</string>
<string name="screen_join_room_join_restricted_message">"Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni."</string>
<string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string> <string name="screen_join_room_knock_action">"Wyślij prośbę o dołączenie"</string>
<string name="screen_join_room_knock_message_description">"Wiadomość (opcjonalne)"</string> <string name="screen_join_room_knock_message_description">"Wiadomość (opcjonalne)"</string>
<string name="screen_join_room_knock_sent_description">"Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana."</string> <string name="screen_join_room_knock_sent_description">"Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana."</string>
<string name="screen_join_room_knock_sent_title">"Wysłano prośbę o dołączenie"</string> <string name="screen_join_room_knock_sent_title">"Wysłano prośbę o dołączenie"</string>
<string name="screen_join_room_loading_alert_message">"Nie udało się wyświetlić podglądu pokoju. Może to być spowodowane problemami z siecią lub serwerem."</string>
<string name="screen_join_room_loading_alert_title">"Nie udało nam się wyświetlić podglądu tego pokoju"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web."</string> <string name="screen_join_room_space_not_supported_description">"%1$s jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web."</string>
<string name="screen_join_room_space_not_supported_title">"Przestrzenie nie są jeszcze obsługiwane"</string> <string name="screen_join_room_space_not_supported_title">"Przestrzenie nie są jeszcze obsługiwane"</string>
<string name="screen_join_room_subtitle_knock">"Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy."</string> <string name="screen_join_room_subtitle_knock">"Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy."</string>

View file

@ -1,14 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Foste banido desta sala por %1$s."</string>
<string name="screen_join_room_ban_message">"Foste banido desta sala."</string>
<string name="screen_join_room_ban_reason">"Razão: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string> <string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancelar"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancelar"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Tens a certeza de que queres cancelar o teu pedido de entrada nesta sala?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Tens a certeza de que queres cancelar o teu pedido de entrada nesta sala?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Cancela o pedido de adesão"</string> <string name="screen_join_room_cancel_knock_alert_title">"Cancela o pedido de adesão"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Sim, recusar &amp; bloquear"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Tens a certeza de que queres recusar o convite para entrar nesta sala? Isto também evitará que %1$s te contacte ou te convide para salas."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite &amp; bloquear"</string>
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
<string name="screen_join_room_fail_message">"Falha ao entrar na sala."</string>
<string name="screen_join_room_fail_reason">"A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."</string>
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares nesta sala"</string>
<string name="screen_join_room_join_action">"Entrar na sala"</string> <string name="screen_join_room_join_action">"Entrar na sala"</string>
<string name="screen_join_room_join_restricted_message">"Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar."</string>
<string name="screen_join_room_knock_action">"Bater à porta"</string> <string name="screen_join_room_knock_action">"Bater à porta"</string>
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string> <string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
<string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string> <string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string>
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string> <string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
<string name="screen_join_room_loading_alert_message">"Não conseguimos exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou servidor."</string>
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a pré-visualização desta sala"</string>
<string name="screen_join_room_space_not_supported_description">"A %1$s ainda não funciona com espaços. Podes usá-los na aplicação web."</string> <string name="screen_join_room_space_not_supported_description">"A %1$s ainda não funciona com espaços. Podes usá-los na aplicação web."</string>
<string name="screen_join_room_space_not_supported_title">"Os espaços ainda não estão implementados"</string> <string name="screen_join_room_space_not_supported_title">"Os espaços ainda não estão implementados"</string>
<string name="screen_join_room_subtitle_knock">"Carrega no botão abaixo para notificar um administrador da sala. Poderás entrar quando te aprovarem."</string> <string name="screen_join_room_subtitle_knock">"Carrega no botão abaixo para notificar um administrador da sala. Poderás entrar quando te aprovarem."</string>

View file

@ -17,6 +17,7 @@
<string name="screen_join_room_knock_message_description">"Сообщение (опционально)"</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_description">"Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."</string>
<string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string> <string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string>
<string name="screen_join_room_loading_alert_message">"Не удалось отобразить предварительный просмотр комнаты. Это может быть связано с проблемами сети или сервера."</string>
<string name="screen_join_room_space_not_supported_description">"%1$s еще не поддерживает пространства. Вы можете получить к ним доступ в веб-версии."</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_space_not_supported_title">"Пространства пока не поддерживаются"</string>
<string name="screen_join_room_subtitle_knock">"Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению."</string> <string name="screen_join_room_subtitle_knock">"Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Áno, zrušiť"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Áno, zrušiť"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Ste si istí, že chcete zrušiť svoju žiadosť o vstup do tejto miestnosti?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Ste si istí, že chcete zrušiť svoju žiadosť o vstup do tejto miestnosti?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Zrušiť žiadosť o pripojenie"</string> <string name="screen_join_room_cancel_knock_alert_title">"Zrušiť žiadosť o pripojenie"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Áno, odmietnuť a zablokovať"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Ste si istí, že chcete odmietnuť pozvanie na vstup do tejto miestnosti? To tiež zabráni tomu, aby vás %1$s kontaktoval/a alebo vás pozval/a do miestností."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Odmietnuť pozvánku a zablokovať"</string>
<string name="screen_join_room_decline_and_block_button_title">"Odmietnuť a zablokovať"</string>
<string name="screen_join_room_fail_message">"Pripojenie do miestnosti zlyhalo."</string> <string name="screen_join_room_fail_message">"Pripojenie do miestnosti zlyhalo."</string>
<string name="screen_join_room_fail_reason">"Táto miestnosť je buď len pre pozvaných, alebo môžu existovať obmedzenia na prístup na úrovni priestoru."</string> <string name="screen_join_room_fail_reason">"Táto miestnosť je buď len pre pozvaných, alebo môžu existovať obmedzenia na prístup na úrovni priestoru."</string>
<string name="screen_join_room_forget_action">"Zabudnúť túto miestnosť"</string> <string name="screen_join_room_forget_action">"Zabudnúť túto miestnosť"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Správa (voliteľné)"</string> <string name="screen_join_room_knock_message_description">"Správa (voliteľné)"</string>
<string name="screen_join_room_knock_sent_description">"Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti."</string> <string name="screen_join_room_knock_sent_description">"Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti."</string>
<string name="screen_join_room_knock_sent_title">"Žiadosť o pripojenie bola odoslaná"</string> <string name="screen_join_room_knock_sent_title">"Žiadosť o pripojenie bola odoslaná"</string>
<string name="screen_join_room_loading_alert_message">"Nepodarilo sa zobraziť ukážku miestnosti. Môže to byť spôsobené problémami so sieťou alebo serverom."</string>
<string name="screen_join_room_loading_alert_title">"Ukážku tejto miestnosti sa nepodarilo zobraziť"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s zatiaľ nepodporuje priestory. K priestorom môžete pristupovať na webe."</string> <string name="screen_join_room_space_not_supported_description">"%1$s zatiaľ nepodporuje priestory. K priestorom môžete pristupovať na webe."</string>
<string name="screen_join_room_space_not_supported_title">"Priestory zatiaľ nie sú podporované"</string> <string name="screen_join_room_space_not_supported_title">"Priestory zatiaľ nie sú podporované"</string>
<string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string> <string name="screen_join_room_subtitle_knock">"Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii."</string>

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"Avbryt begäran"</string> <string name="screen_join_room_cancel_knock_action">"Avbryt begäran"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, avbryt"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Är du säker på att du vill avbryta din begäran om att gå med i det här rummet?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Avbryt begäran om att gå med"</string>
<string name="screen_join_room_join_action">"Gå med i rummet"</string> <string name="screen_join_room_join_action">"Gå med i rummet"</string>
<string name="screen_join_room_knock_action">"Knacka för att gå med"</string> <string name="screen_join_room_knock_action">"Knacka för att gå med"</string>
<string name="screen_join_room_knock_message_description">"Meddelande (valfritt)"</string> <string name="screen_join_room_knock_message_description">"Meddelande (valfritt)"</string>

View file

@ -0,0 +1,32 @@
<?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 tarafından bu odadan yasaklandınız."</string>
<string name="screen_join_room_ban_message">"Bu odadan yasaklandın"</string>
<string name="screen_join_room_ban_reason">"Neden: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"İsteği iptal et"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Evet, iptal et"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Bu odaya katılma isteğinizi iptal etmek istediğinizden emin misiniz?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Katılma isteğini iptal et"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Evet, reddet ve engelle"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Daveti reddet ve engelle"</string>
<string name="screen_join_room_decline_and_block_button_title">"Reddet ve engelle"</string>
<string name="screen_join_room_fail_message">"Odaya katılım başarısız oldu."</string>
<string name="screen_join_room_fail_reason">"Bu odaya yalnızca davetle girilebilir veya alan düzeyinde erişim kısıtlamaları olabilir."</string>
<string name="screen_join_room_forget_action">"Bu odayı unut"</string>
<string name="screen_join_room_invite_required_message">"Bu odaya katılmak için bir davete ihtiyacınız var"</string>
<string name="screen_join_room_join_action">"Odaya katıl"</string>
<string name="screen_join_room_join_restricted_message">"Katılmak için davet edilmeniz veya bir alana üye olmanız gerekebilir."</string>
<string name="screen_join_room_knock_action">"Katılma isteği gönder"</string>
<string name="screen_join_room_knock_message_description">"Mesaj (isteğe bağlı)"</string>
<string name="screen_join_room_knock_sent_description">"Talebiniz kabul edilirse odaya katılmanız için bir davet alacaksınız."</string>
<string name="screen_join_room_knock_sent_title">"Katılma isteği gönderildi"</string>
<string name="screen_join_room_loading_alert_message">"Oda önizlemesini görüntüleyemedik. Bunun nedeni ağ veya sunucu sorunları olabilir."</string>
<string name="screen_join_room_loading_alert_title">"Bu oda önizlemesini görüntüleyemedik"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s henüz alanları desteklemiyor. Alanlara web üzerinden erişebilirsiniz."</string>
<string name="screen_join_room_space_not_supported_title">"Alanlar henüz desteklenmiyor"</string>
<string name="screen_join_room_subtitle_knock">"Aşağıdaki düğmeyi tıkladığınızda bir oda yöneticisi bilgilendirilecektir. Onaylandıktan sonra görüşmeye katılabilirsiniz."</string>
<string name="screen_join_room_subtitle_no_preview">"Mesaj geçmişini görüntülemek için bu odaya üye olmanız gerekmektedir."</string>
<string name="screen_join_room_title_knock">"Bu odaya katılmak ister misiniz?"</string>
<string name="screen_join_room_title_no_preview">"Önizleme mevcut değil"</string>
</resources>

View file

@ -1,14 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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_action">"Скасувати запит"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Так, скасувати"</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_description">"Ви впевнені, що бажаєте скасувати свій запит на приєднання до цієї кімнати?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Скасувати запит на приєднання"</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_join_action">"Приєднатися до кімнати"</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_action">"Постукати, щоб приєднатися"</string>
<string name="screen_join_room_knock_message_description">"Повідомлення (необов\'язково)"</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_description">"Ви отримаєте запрошення приєднатися до кімнати, якщо ваш запит буде прийнятий."</string>
<string name="screen_join_room_knock_sent_title">"Запит на приєднання надіслано"</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_description">"%1$s ще не підтримує простори. Ви можете отримати доступ до них у вебверсії."</string>
<string name="screen_join_room_space_not_supported_title">"Простори поки що не підтримуються"</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_knock">"Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення."</string>

View file

@ -7,6 +7,10 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string> <string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string> <string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string> <string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Yes, decline &amp; block"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Decline invite &amp; block"</string>
<string name="screen_join_room_decline_and_block_button_title">"Decline and block"</string>
<string name="screen_join_room_fail_message">"Joining the room failed."</string> <string name="screen_join_room_fail_message">"Joining the room failed."</string>
<string name="screen_join_room_fail_reason">"This room is either invite-only or there might be restrictions to access at space level."</string> <string name="screen_join_room_fail_reason">"This room is either invite-only or there might be restrictions to access at space level."</string>
<string name="screen_join_room_forget_action">"Forget this room"</string> <string name="screen_join_room_forget_action">"Forget this room"</string>
@ -17,6 +21,8 @@
<string name="screen_join_room_knock_message_description">"Message (optional)"</string> <string name="screen_join_room_knock_message_description">"Message (optional)"</string>
<string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string> <string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string>
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string> <string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
<string name="screen_join_room_loading_alert_message">"We could not display the room preview. This may be due to network or server issues."</string>
<string name="screen_join_room_loading_alert_title">"We couldnt display this room preview"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string> <string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string> <string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. Youll be able to join the conversation once approved."</string> <string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. Youll be able to join the conversation once approved."</string>

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.joinroom.impl
import io.element.android.features.joinroom.impl.di.ForgetRoom
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.simulateLongTask
class FakeForgetRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : ForgetRoom {
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
lambda(roomId)
}
}

View file

@ -13,6 +13,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.joinroom.impl.di.CancelKnockRoom import io.element.android.features.joinroom.impl.di.CancelKnockRoom
import io.element.android.features.joinroom.impl.di.ForgetRoom
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -24,16 +25,23 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomPreview
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
@ -43,12 +51,14 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Optional import java.util.Optional
@Suppress("LargeClass")
class JoinRoomPresenterTest { class JoinRoomPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -58,12 +68,10 @@ class JoinRoomPresenterTest {
val presenter = createJoinRoomPresenter() val presenter = createJoinRoomPresenter()
presenter.test { presenter.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())) assertThat(state.contentState).isEqualTo(ContentState.Loading)
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.applicationName).isEqualTo("AppName")
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -88,7 +96,7 @@ class JoinRoomPresenterTest {
assertThat(contentState.name).isEqualTo(roomSummary.info.name) assertThat(contentState.name).isEqualTo(roomSummary.info.name)
assertThat(contentState.topic).isEqualTo(roomSummary.info.topic) assertThat(contentState.topic).isEqualTo(roomSummary.info.topic)
assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias) assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount) assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.joinedMembersCount)
assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect) assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect)
assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl)
} }
@ -226,9 +234,79 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when room is joined with unauthorized error, then the authorisation status is unauthorized`() = runTest {
val roomDescription = aRoomDescription()
val presenter = createJoinRoomPresenter(
roomDescription = Optional.of(roomDescription),
joinRoomLambda = { _, _, _ ->
Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden"))
},
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin))
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unauthorized)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(
info = aRoomPreviewInfo(
roomId = A_ROOM_ID,
joinRule = JoinRule.Public,
currentUserMembership = CurrentUserMembership.BANNED,
),
roomMembershipDetails = {
Result.success(
RoomMembershipDetails(
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
)
)
}
)
)
}
).apply {
getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomSummary))
}
}
val presenter = createJoinRoomPresenter(
matrixClient = matrixClient
)
presenter.test {
// Skip initial state
skipItems(1)
// Advance until the room info is loaded and the presenter recomposes. The room preview info still needs to be loaded async.
skipItems(1)
// Now we should have the room info
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java)
}
}
}
@Test @Test
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomSummaryFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomSummary)) flowOf(Optional.of(roomSummary))
@ -246,8 +324,8 @@ class JoinRoomPresenterTest {
} }
@Test @Test
fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { fun `present - when room is left and join rule null then join authorization is equal to Unknown`() = runTest {
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomSummaryFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomSummary)) flowOf(Optional.of(roomSummary))
@ -327,6 +405,20 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when room preview join rule is Private then join authorization is equal to NeedInvite`() = runTest {
val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.UNKNOWN)
val presenter = createJoinRoomPresenter(
roomDescription = Optional.of(roomDescription)
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
}
}
}
@Test @Test
fun `present - emit knock room event`() = runTest { fun `present - emit knock room event`() = runTest {
val knockMessage = "Knock message" val knockMessage = "Knock message"
@ -405,24 +497,60 @@ class JoinRoomPresenterTest {
.with(value(A_ROOM_ID)) .with(value(A_ROOM_ID))
} }
@Test
fun `present - emit forget room event`() = runTest {
val forgetRoomSuccess = lambdaRecorder { _: RoomId ->
Result.success(Unit)
}
val forgetRoomFailure = lambdaRecorder { _: RoomId ->
Result.failure<Unit>(RuntimeException("Failed to forget room"))
}
val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess)
val presenter = createJoinRoomPresenter(forgetRoom = fakeForgetRoom)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit))
fakeForgetRoom.lambda = forgetRoomFailure
state.eventSink(JoinRoomEvents.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.forgetAction).isInstanceOf(AsyncAction.Failure::class.java)
}
}
assert(forgetRoomFailure)
.isCalledOnce()
.with(value(A_ROOM_ID))
assert(forgetRoomSuccess)
.isCalledOnce()
.with(value(A_ROOM_ID))
}
@Test @Test
fun `present - when room is not known RoomPreview is loaded`() = runTest { fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewInfoResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.success( Result.success(
RoomPreviewInfo( aRoomPreview(
roomId = A_ROOM_ID, info = aRoomPreviewInfo(
canonicalAlias = RoomAlias("#alias:matrix.org"), roomId = A_ROOM_ID,
name = "Room name", canonicalAlias = RoomAlias("#alias:matrix.org"),
topic = "Room topic", name = "Room name",
avatarUrl = "avatarUrl", topic = "Room topic",
numberOfJoinedMembers = 2, avatarUrl = "avatarUrl",
roomType = RoomType.Room, numberOfJoinedMembers = 2,
isHistoryWorldReadable = false, isSpace = false,
isJoined = false, isHistoryWorldReadable = false,
isInvited = false, joinRule = JoinRule.Public,
isPublic = true, currentUserMembership = null,
canKnock = false, )
) )
) )
} }
@ -450,10 +578,130 @@ class JoinRoomPresenterTest {
} }
} }
@Test
fun `present - when room is not known RoomPreview is loaded as Private`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Private))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded as Custom`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom")))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded as Invite`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Invite))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded as KnockRestricted`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList())))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanKnock)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded as Restricted`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList())))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Restricted)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded as Space`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(isSpace = true))
)
}
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsSpace("AppName"))
}
}
}
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest { fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewInfoResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} }
) )
@ -464,35 +712,27 @@ class JoinRoomPresenterTest {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo( assertThat(state.contentState).isEqualTo(
ContentState.Failure( ContentState.Failure(error = AN_EXCEPTION)
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
error = AN_EXCEPTION
)
) )
state.eventSink(JoinRoomEvents.RetryFetchingContent) state.eventSink(JoinRoomEvents.RetryFetchingContent)
} }
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo( assertThat(state.contentState).isEqualTo(ContentState.Loading)
ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())
)
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo( assertThat(state.contentState).isEqualTo(
ContentState.Failure( ContentState.Failure(error = AN_EXCEPTION)
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
error = AN_EXCEPTION
)
) )
} }
} }
} }
@Test @Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest { fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest {
val client = FakeMatrixClient( val client = FakeMatrixClient(
getRoomPreviewInfoResult = { _, _ -> getRoomPreviewResult = { _, _ ->
Result.failure(Exception("403")) Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden"))
} }
) )
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -501,11 +741,7 @@ class JoinRoomPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo( assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom)
ContentState.UnknownRoom(
roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(),
)
)
} }
} }
} }
@ -521,6 +757,7 @@ class JoinRoomPresenterTest {
}, },
knockRoom: KnockRoom = FakeKnockRoom(), knockRoom: KnockRoom = FakeKnockRoom(),
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(), cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
forgetRoom: ForgetRoom = FakeForgetRoom(),
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() } acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
): JoinRoomPresenter { ): JoinRoomPresenter {
@ -534,6 +771,7 @@ class JoinRoomPresenterTest {
joinRoom = FakeJoinRoom(joinRoomLambda), joinRoom = FakeJoinRoom(joinRoomLambda),
knockRoom = knockRoom, knockRoom = knockRoom,
cancelKnockRoom = cancelKnockRoom, cancelKnockRoom = cancelKnockRoom,
forgetRoom = forgetRoom,
buildMeta = buildMeta, buildMeta = buildMeta,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
) )

View file

@ -178,7 +178,7 @@ class JoinRoomViewTest {
} }
@Test @Test
fun `clicking on Go back when a space is displayed invokes the expected callback`() { fun `clicking on ok when a space is displayed invokes the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setJoinRoomView( rule.setJoinRoomView(
@ -188,9 +188,38 @@ class JoinRoomViewTest {
), ),
onBackClick = it onBackClick = it
) )
rule.clickOn(CommonStrings.action_go_back) rule.clickOn(CommonStrings.action_ok)
} }
} }
@Test
fun `clicking on ok when user is unauthorized the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce {
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(),
joinAction = AsyncAction.Failure(JoinRoomFailures.UnauthorizedJoin),
eventSink = eventsRecorder,
),
onBackClick = it
)
rule.clickOn(CommonStrings.action_ok)
}
}
@Test
fun `clicking on forget when user is banned invokes the expected callback`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_forget_action)
eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
}
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
@ -199,6 +228,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
onJoinSuccess: () -> Unit = EnsureNeverCalled(), onJoinSuccess: () -> Unit = EnsureNeverCalled(),
onKnockSuccess: () -> Unit = EnsureNeverCalled(), onKnockSuccess: () -> Unit = EnsureNeverCalled(),
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(), onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
onForgetSuccess: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
JoinRoomView( JoinRoomView(
@ -206,7 +236,8 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
onBackClick = onBackClick, onBackClick = onBackClick,
onJoinSuccess = onJoinSuccess, onJoinSuccess = onJoinSuccess,
onKnockSuccess = onKnockSuccess, onKnockSuccess = onKnockSuccess,
onCancelKnockSuccess = onCancelKnockSuccess onForgetSuccess = onForgetSuccess,
onCancelKnockSuccess = onCancelKnockSuccess,
) )
} }
} }

View file

@ -128,14 +128,14 @@ private fun KnockRequestsBannerContent(
Text( Text(
text = state.formattedTitle(), text = state.formattedTitle(),
style = ElementTheme.typography.fontBodyMdMedium, style = ElementTheme.typography.fontBodyMdMedium,
color = MaterialTheme.colorScheme.primary, color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
) )
if (state.subtitle != null) { if (state.subtitle != null) {
Text( Text(
text = state.subtitle, text = state.subtitle,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = MaterialTheme.colorScheme.secondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
) )
} }

View file

@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -331,7 +330,7 @@ private fun KnockRequestItem(
text = knockRequest.getBestName(), text = knockRequest.getBestName(),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary, color = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
) )
val formattedDate = knockRequest.formattedDate val formattedDate = knockRequest.formattedDate
@ -339,7 +338,7 @@ private fun KnockRequestItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = formattedDate, text = formattedDate,
color = MaterialTheme.colorScheme.secondary, color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
) )
} }
@ -348,7 +347,7 @@ private fun KnockRequestItem(
if (!knockRequest.displayName.isNullOrEmpty()) { if (!knockRequest.displayName.isNullOrEmpty()) {
Text( Text(
text = knockRequest.userId.value, text = knockRequest.userId.value,
color = MaterialTheme.colorScheme.secondary, color = ElementTheme.colors.textSecondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_single_knock_request_accept_button_title">"Прыняць"</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_single_knock_request_accept_button_title">"Приемане"</string>
</resources>

View file

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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_single_knock_request_accept_button_title">"پذیرش"</string>
</resources>

View file

@ -24,7 +24,7 @@
<string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string> <string name="screen_knock_requests_list_empty_state_description">"Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."</string>
<string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string> <string name="screen_knock_requests_list_empty_state_title">"Nincs függőben lévő csatlakozási kérelem"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Csatlakozási kérések betöltése…"</string> <string name="screen_knock_requests_list_initial_loading_title">"Csatlakozási kérések betöltése…"</string>
<string name="screen_knock_requests_list_title">"Csatlakozási kérelmek"</string> <string name="screen_knock_requests_list_title">"Csatlakozási kérelem"</string>
<plurals name="screen_room_multiple_knock_requests_title"> <plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item> <item quantity="one">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>
<item quantity="other">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item> <item quantity="other">"%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"</item>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_single_knock_request_accept_button_title">"მიღება"</string>
</resources>

View file

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

View file

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

View file

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

View file

@ -4,15 +4,26 @@
<string name="screen_knock_requests_list_accept_all_alert_description">"Tens a certeza de que queres aceitar todos os pedidos de entrada?"</string> <string name="screen_knock_requests_list_accept_all_alert_description">"Tens a certeza de que queres aceitar todos os pedidos de entrada?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"Aceitar todos os pedidos"</string> <string name="screen_knock_requests_list_accept_all_alert_title">"Aceitar todos os pedidos"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"Aceitar todos"</string> <string name="screen_knock_requests_list_accept_all_button_title">"Aceitar todos"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Não foi possível aceitar todos os pedidos. Queres tentar novamente?"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Falha ao aceitar todos os pedidos"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"A aceitar todos os pedidos de entrada"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"Não foi possível aceitar este pedido. Queres tentar novamente?"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"Falha ao aceitar pedido"</string>
<string name="screen_knock_requests_list_accept_loading_title">"A aceitar pedido de entrada"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Sim, recusar e proibir"</string> <string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Sim, recusar e proibir"</string>
<string name="screen_knock_requests_list_ban_alert_description">"Tens a certeza de que queres recusar e banir %1$s? Este utilizador não poderá voltar a pedir para entrar nesta sala."</string> <string name="screen_knock_requests_list_ban_alert_description">"Tens a certeza de que queres recusar e banir %1$s? Este utilizador não poderá voltar a pedir para entrar nesta sala."</string>
<string name="screen_knock_requests_list_ban_alert_title">"Recusar e banir"</string> <string name="screen_knock_requests_list_ban_alert_title">"Recusar e banir"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Sim, recusar"</string> <string name="screen_knock_requests_list_ban_loading_title">"A rejeitar pedido e a banir o utilizador"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Sim, rejeitar"</string>
<string name="screen_knock_requests_list_decline_alert_description">"Tens a certeza que queres recusar o pedido de entrada de %1$s?"</string> <string name="screen_knock_requests_list_decline_alert_description">"Tens a certeza que queres recusar o pedido de entrada de %1$s?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"Recusar entrada"</string> <string name="screen_knock_requests_list_decline_alert_title">"Rejeitar entrada"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Recusar e banir"</string> <string name="screen_knock_requests_list_decline_and_ban_action_title">"Recusar e banir"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"Não foi possível rejeitar este pedido. Queres tentar novamente?"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"Falha ao rejeitar pedido"</string>
<string name="screen_knock_requests_list_decline_loading_title">"A rejeitar pedido de entrada"</string>
<string name="screen_knock_requests_list_empty_state_description">"Quando alguém pedir para entrar na sala, irás poder rever o pedido aqui."</string> <string name="screen_knock_requests_list_empty_state_description">"Quando alguém pedir para entrar na sala, irás poder rever o pedido aqui."</string>
<string name="screen_knock_requests_list_empty_state_title">"Sem pedidos de entrada"</string> <string name="screen_knock_requests_list_empty_state_title">"Sem pedidos de entrada"</string>
<string name="screen_knock_requests_list_initial_loading_title">"A carregar pedidos de entrada…"</string>
<string name="screen_knock_requests_list_title">"Pedidos de entrada"</string> <string name="screen_knock_requests_list_title">"Pedidos de entrada"</string>
<plurals name="screen_room_multiple_knock_requests_title"> <plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d outro querem entrar nesta sala"</item> <item quantity="one">"%1$s +%2$d outro querem entrar nesta sala"</item>

View file

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

View file

@ -24,14 +24,14 @@
<string name="screen_knock_requests_list_empty_state_description">"Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате."</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_empty_state_title">"Нет ожидающих запросов на присоединение"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Загрузка запросов на присоединение…"</string> <string name="screen_knock_requests_list_initial_loading_title">"Загрузка запросов на присоединение…"</string>
<string name="screen_knock_requests_list_title">"Запросы на присоединение"</string> <string name="screen_knock_requests_list_title">"Запросы на вступление"</string>
<plurals name="screen_room_multiple_knock_requests_title"> <plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d хочет присоединиться к этой комнате"</item> <item quantity="one">"%1$s +%2$d хочет присоединиться к этой комнате"</item>
<item quantity="few">"%1$s +%2$d хотят присоединиться к этой комнате"</item> <item quantity="few">"%1$s +%2$d хотят присоединиться к этой комнате"</item>
<item quantity="many">"%1$s +%2$d хотят присоединиться к этой комнате"</item> <item quantity="many">"%1$s +%2$d хотят присоединиться к этой комнате"</item>
</plurals> </plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Показать все"</string> <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_accept_button_title">"Разрешить"</string>
<string name="screen_room_single_knock_request_title">"%1$s хочет присоединиться к этой комнате"</string> <string name="screen_room_single_knock_request_title">"%1$s хочет присоединиться к этой комнате"</string>
<string name="screen_room_single_knock_request_view_button_title">"Просмотр"</string> <string name="screen_room_single_knock_request_view_button_title">"Просмотр"</string>
</resources> </resources>

View file

@ -24,7 +24,7 @@
<string name="screen_knock_requests_list_empty_state_description">"Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."</string> <string name="screen_knock_requests_list_empty_state_description">"Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."</string>
<string name="screen_knock_requests_list_empty_state_title">"Žiadna čakajúca žiadosť o pripojenie"</string> <string name="screen_knock_requests_list_empty_state_title">"Žiadna čakajúca žiadosť o pripojenie"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Načítavajú sa žiadosti o pripojenie…"</string> <string name="screen_knock_requests_list_initial_loading_title">"Načítavajú sa žiadosti o pripojenie…"</string>
<string name="screen_knock_requests_list_title">"Žiadosti o pripojenie"</string> <string name="screen_knock_requests_list_title">"Žiadosti o vstup"</string>
<plurals name="screen_room_multiple_knock_requests_title"> <plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item> <item quantity="one">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item>
<item quantity="few">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item> <item quantity="few">"%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"</item>

View file

@ -0,0 +1,31 @@
<?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">"Ja, acceptera alla"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"Är du säker på att du vill acceptera alla förfrågningar om att gå med?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"Acceptera alla förfrågningar"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"Acceptera alla"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Misslyckades att acceptera alla förfrågningar"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"Accepterar alla förfrågningar om att gå med"</string>
<string name="screen_knock_requests_list_accept_loading_title">"Accepterar begäran om att gå med"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ja, avslå och förbjud"</string>
<string name="screen_knock_requests_list_ban_alert_description">"Är du säker på att du vill avvisa och förbjuda%1$s? Den här användaren kommer inte att kunna begära åtkomst för att gå med i det här rummet igen."</string>
<string name="screen_knock_requests_list_ban_alert_title">"Avvisa och förbjud åtkomst"</string>
<string name="screen_knock_requests_list_ban_loading_title">"Avvisar och bannar åtkomst"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ja, avböj"</string>
<string name="screen_knock_requests_list_decline_alert_description">"Är du säker på att du vill avslå %1$s begäran om att gå med i det här rummet?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"Avvisa åtkomst"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Avvisa och förbjud"</string>
<string name="screen_knock_requests_list_decline_loading_title">"Avvisa begäran om att gå med"</string>
<string name="screen_knock_requests_list_empty_state_description">"När någon begär om att gå med i rummet, kan du se deras förfrågan här."</string>
<string name="screen_knock_requests_list_empty_state_title">"Ingen väntande begäran om att gå med"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Laddar förfrågningar om att gå med …"</string>
<string name="screen_knock_requests_list_title">"Begäran om att gå med"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s+ %2$d annan vill gå med i detta rum"</item>
<item quantity="other">"%1$s+ %2$d andra vill gå med i detta rum"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Visa alla"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Godkänn"</string>
<string name="screen_room_single_knock_request_title">"%1$s vill gå med i det här rummet"</string>
<string name="screen_room_single_knock_request_view_button_title">"Visa"</string>
</resources>

View file

@ -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_knock_requests_list_accept_all_alert_confirm_button_title">"Evet, tümünü kabul et"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"Tüm katılma isteklerini kabul etmek istediğinizden emin misiniz?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"Tüm istekleri kabul et"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"Tümünü kabul et"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Tüm istekleri kabul edemedik. Tekrar denemek ister misiniz?"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Tüm istekler kabul edilemedi"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"Tüm katılım istekleri kabul ediliyor"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"Bu isteği kabul edemedik. Tekrar denemek ister misiniz?"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"İstek kabul edilemedi"</string>
<string name="screen_knock_requests_list_accept_loading_title">"Katılma isteği kabul ediliyor"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Evet, reddet ve yasakla"</string>
<string name="screen_knock_requests_list_ban_alert_description">"%1$s reddetmek ve yasaklamak istediğinizden emin misiniz? Bu kullanıcı, bu odaya tekrar katılmak için erişim isteğinde bulunamaz."</string>
<string name="screen_knock_requests_list_ban_alert_title">"Reddet ve erişimi yasakla"</string>
<string name="screen_knock_requests_list_ban_loading_title">"Reddediliyor ve erişim yasaklanıyor"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Evet, reddet"</string>
<string name="screen_knock_requests_list_decline_alert_description">"Bu odaya katılma isteğini reddetmek istediğinizden emin misiniz %1$s?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"Erişimi reddet"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Reddet ve yasakla"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"Bu isteği reddedemedik. Tekrar denemek ister misiniz?"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"İsteği reddetme başarısız oldu"</string>
<string name="screen_knock_requests_list_decline_loading_title">"Katılma isteği reddediliyor"</string>
<string name="screen_knock_requests_list_empty_state_description">"Birisi odaya katılmak istediğinde, isteklerini burada görebileceksiniz."</string>
<string name="screen_knock_requests_list_empty_state_title">"Bekleyen katılım isteği yok"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Katılma istekleri yükleniyor…"</string>
<string name="screen_knock_requests_list_title">"Katılma istekleri"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d kişi daha bu odaya katılmak istiyor"</item>
<item quantity="other">"%1$s +%2$d kişi daha bu odaya katılmak istiyor"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Tümünü görüntüle"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Kabul et"</string>
<string name="screen_room_single_knock_request_title">"%1$s bu odaya katılmak istiyor"</string>
<string name="screen_room_single_knock_request_view_button_title">"Görüntüle"</string>
</resources>

View file

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

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