Merge remote-tracking branch 'origin/develop' into feature/bma/mutliAccountNotification
This commit is contained in:
commit
e96cd9e28f
421 changed files with 4365 additions and 4190 deletions
|
|
@ -14,15 +14,14 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface AccountSelectEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onSelectAccount(sessionId: SessionId)
|
||||
fun onAccountSelected(sessionId: SessionId)
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.architecture.callback
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
|
|
@ -26,23 +26,15 @@ class AccountSelectNode(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AccountSelectPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val callbacks = plugins.filterIsInstance<AccountSelectEntryPoint.Callback>()
|
||||
|
||||
private fun onDismiss() {
|
||||
callbacks.forEach { it.onCancel() }
|
||||
}
|
||||
|
||||
private fun onSelectAccount(sessionId: SessionId) {
|
||||
callbacks.forEach { it.onSelectAccount(sessionId) }
|
||||
}
|
||||
private val callback: AccountSelectEntryPoint.Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
AccountSelectView(
|
||||
state = state,
|
||||
onDismiss = ::onDismiss,
|
||||
onSelectAccount = ::onSelectAccount,
|
||||
onDismiss = callback::onCancel,
|
||||
onSelectAccount = callback::onAccountSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.accountselect.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
|
|
@ -17,18 +16,11 @@ import io.element.android.libraries.architecture.createNode
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAccountSelectEntryPoint : AccountSelectEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): AccountSelectEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : AccountSelectEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: AccountSelectEntryPoint.Callback): AccountSelectEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<AccountSelectNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: AccountSelectEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<AccountSelectNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ class DefaultAccountSelectEntryPointTest {
|
|||
)
|
||||
}
|
||||
val callback = object : AccountSelectEntryPoint.Callback {
|
||||
override fun onSelectAccount(sessionId: SessionId) = lambdaError()
|
||||
override fun onAccountSelected(sessionId: SessionId) = lambdaError()
|
||||
override fun onCancel() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(AccountSelectNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.libraries.architecture
|
||||
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
|
||||
inline fun <reified I : Plugin> Node.callback(): I {
|
||||
return requireNotNull(plugins<I>().singleOrNull()) { "Make sure to actually pass a Callback plugin to your node" }
|
||||
}
|
||||
|
|
@ -18,12 +18,12 @@ android {
|
|||
testOptions {
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.showkase)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.test.roborazzi)
|
||||
testImplementation(libs.test.roborazzi.compose)
|
||||
testImplementation(libs.test.roborazzi.junit)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.showkase)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.test.roborazzi)
|
||||
testImplementation(libs.test.roborazzi.compose)
|
||||
testImplementation(libs.test.roborazzi.junit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.cryptography.test"
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.cryptography.api)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.cryptography.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.dateformatter.api"
|
||||
|
||||
dependencies {
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,19 +30,19 @@ android {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.dateformatter.test"
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,24 +25,24 @@ android {
|
|||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.compound)
|
||||
|
||||
implementation(libs.androidx.compose.material3.windowsizeclass)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
implementation(libs.showkase)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.compound)
|
||||
|
||||
implementation(libs.androidx.compose.material3.windowsizeclass)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
implementation(libs.showkase)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,8 +114,7 @@ enum class FeatureFlags(
|
|||
title = "Sync notifications with WorkManager",
|
||||
description = "Use WorkManager to schedule notification sync tasks when a push is received." +
|
||||
" This should improve reliability and battery usage.",
|
||||
// Enable by default on nightly and debug builds so we can get feedback before enabling it for everyone.
|
||||
defaultValue = { meta -> meta.buildType != BuildType.RELEASE },
|
||||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.featureflag.test"
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.test)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.test)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.libraries.matrix.api.auth
|
||||
|
||||
/**
|
||||
* Checks the homeserver's compatibility with Element X.
|
||||
*/
|
||||
interface HomeServerLoginCompatibilityChecker {
|
||||
/**
|
||||
* Performs the compatibility check given the homeserver's [url].
|
||||
* @return a `true` value if the homeserver is compatible, `false` if not, or a failure result if the check unexpectedly failed.
|
||||
*/
|
||||
suspend fun check(url: String): Result<Boolean>
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import org.matrix.rustcomponents.sdk.RequestConfig
|
|||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
|
||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_crypto.CollectStrategy
|
||||
|
|
@ -105,12 +106,13 @@ class RustMatrixClientFactory(
|
|||
slidingSyncType: ClientBuilderSlidingSync,
|
||||
): ClientBuilder {
|
||||
return clientBuilderProvider.provide()
|
||||
.sessionPaths(
|
||||
dataPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
.sqliteStore(
|
||||
SqliteStoreBuilder(
|
||||
dataPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
).passphrase(passphrase)
|
||||
)
|
||||
.setSessionDelegate(sessionDelegate)
|
||||
.sessionPassphrase(passphrase)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.autoEnableBackups(true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.libraries.matrix.impl.auth
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class RustHomeServerLoginCompatibilityChecker(
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
) : HomeServerLoginCompatibilityChecker {
|
||||
override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions {
|
||||
clientBuilderProvider.provide()
|
||||
.inMemoryStore()
|
||||
.serverNameOrHomeserverUrl(url)
|
||||
.build()
|
||||
.use {
|
||||
it.homeserverLoginDetails()
|
||||
}
|
||||
.use {
|
||||
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
|
||||
it.supportsOidcLogin() || it.supportsPasswordLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -285,7 +285,6 @@ class RustMatrixAuthenticationService(
|
|||
runCatchingExceptions {
|
||||
val client = makeQrCodeLoginClient(
|
||||
sessionPaths = emptySessionPaths,
|
||||
passphrase = pendingPassphrase,
|
||||
qrCodeData = sdkQrCodeLoginData,
|
||||
)
|
||||
client.loginWithQrCode(
|
||||
|
|
@ -344,7 +343,6 @@ class RustMatrixAuthenticationService(
|
|||
|
||||
private suspend fun makeQrCodeLoginClient(
|
||||
sessionPaths: SessionPaths,
|
||||
passphrase: String?,
|
||||
qrCodeData: QrCodeData,
|
||||
): Client {
|
||||
Timber.d("Creating client for QR Code login with simplified sliding sync")
|
||||
|
|
@ -354,7 +352,6 @@ class RustMatrixAuthenticationService(
|
|||
passphrase = pendingPassphrase,
|
||||
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
||||
)
|
||||
.sessionPassphrase(passphrase)
|
||||
.serverNameOrHomeserverUrl(qrCodeData.serverName()!!)
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class NotificationMapper(
|
|||
isDirect = item.roomInfo.isDirect,
|
||||
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
||||
)
|
||||
val timestamp = item.timestamp() ?: clock.epochMillis()
|
||||
NotificationData(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
|
|
@ -53,8 +54,8 @@ class NotificationMapper(
|
|||
isDm = isDm,
|
||||
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
|
||||
isNoisy = item.isNoisy.orFalse(),
|
||||
timestamp = item.timestamp() ?: clock.epochMillis(),
|
||||
content = item.event.use { notificationContentMapper.map(it) }.getOrThrow(),
|
||||
timestamp = timestamp,
|
||||
content = notificationContentMapper.map(item.event).getOrThrow(),
|
||||
hasMention = item.hasMention.orFalse(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ class TimelineEventToNotificationContentMapper {
|
|||
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
|
||||
return runCatchingExceptions {
|
||||
timelineEvent.use {
|
||||
val senderId = UserId(timelineEvent.senderId())
|
||||
timelineEvent.eventType().use { eventType ->
|
||||
eventType.toContent(senderId = UserId(timelineEvent.senderId()))
|
||||
eventType.toContent(senderId = senderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.libraries.matrix.impl.auth
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@Ignore("JNA direct mapping has broken unit tests with FFI fakes")
|
||||
class RustHomeserverLoginCompatibilityCheckerTest {
|
||||
@Test
|
||||
fun `check - is valid if it supports OIDC login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOidcLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is valid if it supports password login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsPasswordLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is not valid if it only supports SSO login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsSsoLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is not valid if fetching the data fails`() = runTest {
|
||||
val sut = createChecker { error("Unexpected error!") }
|
||||
assertThat(sut.check("https://matrix.host.org").isFailure).isTrue()
|
||||
}
|
||||
|
||||
private fun createChecker(
|
||||
result: () -> FakeFfiHomeserverLoginDetails,
|
||||
) = RustHomeServerLoginCompatibilityChecker(
|
||||
clientBuilderProvider = FakeClientBuilderProvider {
|
||||
FakeFfiClientBuilder {
|
||||
FakeFfiClient(homeserverLoginDetailsResult = result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import org.matrix.rustcomponents.sdk.ClientSessionDelegate
|
|||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.RequestConfig
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
|
||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder
|
||||
import uniffi.matrix_sdk.BackupDownloadStrategy
|
||||
import uniffi.matrix_sdk_crypto.CollectStrategy
|
||||
import uniffi.matrix_sdk_crypto.DecryptionSettings
|
||||
|
|
@ -29,7 +30,6 @@ class FakeFfiClientBuilder(
|
|||
override fun decryptionSettings(decryptionSettings: DecryptionSettings): ClientBuilder = this
|
||||
override fun disableSslVerification() = this
|
||||
override fun homeserverUrl(url: String) = this
|
||||
override fun sessionPassphrase(passphrase: String?) = this
|
||||
override fun proxy(url: String) = this
|
||||
override fun requestConfig(config: RequestConfig) = this
|
||||
override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this
|
||||
|
|
@ -42,5 +42,6 @@ class FakeFfiClientBuilder(
|
|||
override fun username(username: String) = this
|
||||
override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this
|
||||
override fun threadsEnabled(enabled: Boolean, threadSubscriptions: Boolean): ClientBuilder = this
|
||||
override fun sqliteStore(config: SqliteStoreBuilder): ClientBuilder = this
|
||||
override suspend fun build() = buildResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ import org.matrix.rustcomponents.sdk.NoHandle
|
|||
|
||||
class FakeFfiHomeserverLoginDetails(
|
||||
private val url: String = "https://example.org",
|
||||
private val supportsPasswordLogin: Boolean = true,
|
||||
private val supportsOidcLogin: Boolean = false
|
||||
private val supportsPasswordLogin: Boolean = false,
|
||||
private val supportsOidcLogin: Boolean = false,
|
||||
private val supportsSsoLogin: Boolean = false,
|
||||
) : HomeserverLoginDetails(NoHandle) {
|
||||
override fun url(): String = url
|
||||
override fun supportsOidcLogin(): Boolean = supportsOidcLogin
|
||||
override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin
|
||||
override fun supportsSsoLogin(): Boolean = supportsSsoLogin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.libraries.matrix.test.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
|
||||
class FakeHomeServerLoginCompatibilityChecker(
|
||||
private val checkResult: (String) -> Result<Boolean>,
|
||||
) : HomeServerLoginCompatibilityChecker {
|
||||
override suspend fun check(url: String): Result<Boolean> {
|
||||
return checkResult(url)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,12 +13,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers.api"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ setupDependencyInjection()
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers.test"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.mediapickers.api)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.mediapickers.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,15 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
interface MediaGalleryEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onBackClick()
|
||||
fun onViewInTimeline(eventId: EventId)
|
||||
fun forwardEvent(eventId: EventId)
|
||||
fun viewInTimeline(eventId: EventId)
|
||||
fun forward(eventId: EventId, fromPinnedEvents: Boolean)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,19 +19,19 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface MediaViewerEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: Params,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun avatar(filename: String, avatarUrl: String): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createParamsForAvatar(filename: String, avatarUrl: String): Params
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
fun onViewInTimeline(eventId: EventId)
|
||||
fun onForwardEvent(eventId: EventId)
|
||||
fun viewInTimeline(eventId: EventId)
|
||||
fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.mediaviewer.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -18,18 +17,14 @@ import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFl
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMediaGalleryEntryPoint : MediaGalleryEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaGalleryEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : MediaGalleryEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: MediaGalleryEntryPoint.Callback): MediaGalleryEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<MediaGalleryFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: MediaGalleryEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<MediaGalleryFlowNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.mediaviewer.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -22,52 +21,42 @@ import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMediaViewerEntryPoint : MediaViewerEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaViewerEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
override fun createParamsForAvatar(filename: String, avatarUrl: String): MediaViewerEntryPoint.Params {
|
||||
// We need to fake the MimeType here for the viewer to work.
|
||||
val mimeType = MimeTypes.Images
|
||||
return MediaViewerEntryPoint.Params(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
eventId = null,
|
||||
mediaInfo = MediaInfo(
|
||||
filename = filename,
|
||||
fileSize = null,
|
||||
caption = null,
|
||||
mimeType = mimeType,
|
||||
formattedFileSize = "",
|
||||
fileExtension = "",
|
||||
senderId = UserId("@dummy:server.org"),
|
||||
senderName = null,
|
||||
senderAvatar = null,
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(url = avatarUrl),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = false,
|
||||
)
|
||||
}
|
||||
|
||||
return object : MediaViewerEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: MediaViewerEntryPoint.Callback): MediaViewerEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun params(params: MediaViewerEntryPoint.Params): MediaViewerEntryPoint.NodeBuilder {
|
||||
plugins += params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun avatar(filename: String, avatarUrl: String): MediaViewerEntryPoint.NodeBuilder {
|
||||
// We need to fake the MimeType here for the viewer to work.
|
||||
val mimeType = MimeTypes.Images
|
||||
return params(
|
||||
MediaViewerEntryPoint.Params(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
eventId = null,
|
||||
mediaInfo = MediaInfo(
|
||||
filename = filename,
|
||||
fileSize = null,
|
||||
caption = null,
|
||||
mimeType = mimeType,
|
||||
formattedFileSize = "",
|
||||
fileExtension = "",
|
||||
senderId = UserId("@dummy:server.org"),
|
||||
senderName = null,
|
||||
senderAvatar = null,
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(url = avatarUrl),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<MediaViewerNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: MediaViewerEntryPoint.Params,
|
||||
callback: MediaViewerEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<MediaViewerNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(params, callback),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
|
||||
|
|
@ -38,33 +38,19 @@ class MediaGalleryNode(
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onBackClick()
|
||||
fun onItemClick(item: MediaItem.Event)
|
||||
fun onViewInTimeline(eventId: EventId)
|
||||
fun onForward(eventId: EventId)
|
||||
fun showItem(item: MediaItem.Event)
|
||||
fun viewInTimeline(eventId: EventId)
|
||||
fun forward(eventId: EventId)
|
||||
}
|
||||
|
||||
private fun onBackClick() {
|
||||
plugins<Callback>().forEach {
|
||||
it.onBackClick()
|
||||
}
|
||||
}
|
||||
private val callback: Callback = callback()
|
||||
|
||||
override fun onViewInTimelineClick(eventId: EventId) {
|
||||
plugins<Callback>().forEach {
|
||||
it.onViewInTimeline(eventId)
|
||||
}
|
||||
callback.viewInTimeline(eventId)
|
||||
}
|
||||
|
||||
override fun onForwardClick(eventId: EventId) {
|
||||
plugins<Callback>().forEach {
|
||||
it.onForward(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(item: MediaItem.Event) {
|
||||
plugins<Callback>().forEach {
|
||||
it.onItemClick(item)
|
||||
}
|
||||
callback.forward(eventId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -75,8 +61,8 @@ class MediaGalleryNode(
|
|||
val state = presenter.present()
|
||||
MediaGalleryView(
|
||||
state = state,
|
||||
onBackClick = ::onBackClick,
|
||||
onItemClick = ::onItemClick,
|
||||
onBackClick = callback::onBackClick,
|
||||
onItemClick = callback::showItem,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.overlay.Overlay
|
||||
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||
|
|
@ -70,41 +70,25 @@ class MediaGalleryFlowNode(
|
|||
) : NavTarget
|
||||
}
|
||||
|
||||
private fun onBackClick() {
|
||||
plugins<MediaGalleryEntryPoint.Callback>().forEach {
|
||||
it.onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onViewInTimeline(eventId: EventId) {
|
||||
plugins<MediaGalleryEntryPoint.Callback>().forEach {
|
||||
it.onViewInTimeline(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardEvent(eventId: EventId) {
|
||||
plugins<MediaGalleryEntryPoint.Callback>().forEach {
|
||||
it.forwardEvent(eventId)
|
||||
}
|
||||
}
|
||||
private val callback: MediaGalleryEntryPoint.Callback = callback()
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
val callback = object : MediaGalleryNode.Callback {
|
||||
override fun onBackClick() {
|
||||
this@MediaGalleryFlowNode.onBackClick()
|
||||
callback.onBackClick()
|
||||
}
|
||||
|
||||
override fun onViewInTimeline(eventId: EventId) {
|
||||
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
|
||||
override fun viewInTimeline(eventId: EventId) {
|
||||
callback.viewInTimeline(eventId)
|
||||
}
|
||||
|
||||
override fun onForward(eventId: EventId) {
|
||||
forwardEvent(eventId)
|
||||
override fun forward(eventId: EventId) {
|
||||
callback.forward(eventId, fromPinnedEvents = false)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MediaItem.Event) {
|
||||
override fun showItem(item: MediaItem.Event) {
|
||||
val mode = when (item) {
|
||||
is MediaItem.Audio,
|
||||
is MediaItem.Voice,
|
||||
|
|
@ -131,28 +115,28 @@ class MediaGalleryFlowNode(
|
|||
overlay.hide()
|
||||
}
|
||||
|
||||
override fun onViewInTimeline(eventId: EventId) {
|
||||
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
|
||||
override fun viewInTimeline(eventId: EventId) {
|
||||
callback.viewInTimeline(eventId)
|
||||
}
|
||||
|
||||
override fun onForwardEvent(eventId: EventId) {
|
||||
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||
// Need to go to the parent because of the overlay
|
||||
forwardEvent(eventId)
|
||||
callback.forward(eventId, fromPinnedEvents)
|
||||
}
|
||||
}
|
||||
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
MediaViewerEntryPoint.Params(
|
||||
mode = navTarget.mode,
|
||||
eventId = navTarget.eventId,
|
||||
mediaInfo = navTarget.mediaInfo,
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
canShowInfo = true,
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
.build()
|
||||
mediaViewerEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = MediaViewerEntryPoint.Params(
|
||||
mode = navTarget.mode,
|
||||
eventId = navTarget.eventId,
|
||||
mediaInfo = navTarget.mediaInfo,
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
canShowInfo = true,
|
||||
),
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
|
||||
interface MediaViewerNavigator {
|
||||
fun onViewInTimelineClick(eventId: EventId)
|
||||
fun onForwardClick(eventId: EventId)
|
||||
fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean)
|
||||
fun onItemDeleted()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
|
|
@ -23,6 +22,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -57,28 +57,19 @@ class MediaViewerNode(
|
|||
private val enterpriseService: EnterpriseService,
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
MediaViewerNavigator {
|
||||
private val callback: MediaViewerEntryPoint.Callback = callback()
|
||||
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
||||
|
||||
private fun onDone() {
|
||||
plugins<MediaViewerEntryPoint.Callback>().forEach {
|
||||
it.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewInTimelineClick(eventId: EventId) {
|
||||
plugins<MediaViewerEntryPoint.Callback>().forEach {
|
||||
it.onViewInTimeline(eventId)
|
||||
}
|
||||
callback.viewInTimeline(eventId)
|
||||
}
|
||||
|
||||
override fun onForwardClick(eventId: EventId) {
|
||||
plugins<MediaViewerEntryPoint.Callback>().forEach {
|
||||
it.onForwardEvent(eventId)
|
||||
}
|
||||
override fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||
callback.forwardEvent(eventId, fromPinnedEvents)
|
||||
}
|
||||
|
||||
override fun onItemDeleted() {
|
||||
onDone()
|
||||
callback.onDone()
|
||||
}
|
||||
|
||||
private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
|
||||
|
|
@ -90,11 +81,7 @@ class MediaViewerNode(
|
|||
timelineMediaGalleryDataSource
|
||||
} else {
|
||||
// Can we use a specific timeline?
|
||||
val timelineMode = when (val mode = inputs.mode) {
|
||||
is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> mode.timelineMode
|
||||
is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> mode.timelineMode
|
||||
else -> null
|
||||
}
|
||||
val timelineMode = inputs.mode.getTimelineMode()
|
||||
when (timelineMode) {
|
||||
null -> timelineMediaGalleryDataSource
|
||||
Timeline.Mode.Live,
|
||||
|
|
@ -153,8 +140,16 @@ class MediaViewerNode(
|
|||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
audioFocus = audioFocus,
|
||||
onBackClick = ::onDone,
|
||||
onBackClick = callback::onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MediaViewerEntryPoint.MediaViewerMode.getTimelineMode(): Timeline.Mode? {
|
||||
return when (this) {
|
||||
is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> timelineMode
|
||||
is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> timelineMode
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
|
|
@ -119,7 +120,10 @@ class MediaViewerPresenter(
|
|||
}
|
||||
is MediaViewerEvents.Forward -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onForwardClick(event.eventId)
|
||||
navigator.onForwardClick(
|
||||
eventId = event.eventId,
|
||||
fromPinnedEvents = inputs.mode.getTimelineMode() == Timeline.Mode.PinnedEvents,
|
||||
)
|
||||
}
|
||||
is MediaViewerEvents.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class SingleMediaGalleryDataSource(
|
|||
override fun start() = Unit
|
||||
override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data))
|
||||
override fun getLastData(): AsyncData<GroupedMediaItems> = AsyncData.Success(data)
|
||||
|
||||
override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit
|
||||
override suspend fun deleteItem(eventId: EventId) = Unit
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ package io.element.android.libraries.mediaviewer.impl
|
|||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFlowNode
|
||||
import io.element.android.libraries.mediaviewer.test.FakeMediaViewerEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
|
|
@ -35,19 +34,19 @@ class DefaultMediaGalleryEntryPointTest {
|
|||
MediaGalleryFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
mediaViewerEntryPoint = object : MediaViewerEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError()
|
||||
},
|
||||
mediaViewerEntryPoint = FakeMediaViewerEntryPoint(),
|
||||
)
|
||||
}
|
||||
val callback = object : MediaGalleryEntryPoint.Callback {
|
||||
override fun onBackClick() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun forwardEvent(eventId: EventId) = lambdaError()
|
||||
override fun viewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun forward(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(MediaGalleryFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,14 +71,16 @@ class DefaultMediaViewerEntryPointTest {
|
|||
}
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun onForwardEvent(eventId: EventId) = lambdaError()
|
||||
override fun viewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||
}
|
||||
val params = createMediaViewerEntryPointParams()
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(MediaViewerNode::class.java)
|
||||
assertThat(result.plugins).contains(params)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
|
|
@ -115,16 +117,19 @@ class DefaultMediaViewerEntryPointTest {
|
|||
}
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun onForwardEvent(eventId: EventId) = lambdaError()
|
||||
override fun viewInTimeline(eventId: EventId) = lambdaError()
|
||||
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.avatar(
|
||||
filename = "fn",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
.callback(callback)
|
||||
.build()
|
||||
val params = entryPoint.createParamsForAvatar(
|
||||
filename = "fn",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(MediaViewerNode::class.java)
|
||||
assertThat(result.plugins).contains(
|
||||
MediaViewerEntryPoint.Params(
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||
|
||||
class FakeMediaViewerNavigator(
|
||||
private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() },
|
||||
private val onForwardClickLambda: (EventId) -> Unit = { lambdaError() },
|
||||
private val onForwardClickLambda: (EventId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
private val onItemDeletedLambda: () -> Unit = { lambdaError() },
|
||||
) : MediaViewerNavigator {
|
||||
override fun onViewInTimelineClick(eventId: EventId) {
|
||||
onViewInTimelineClickLambda(eventId)
|
||||
}
|
||||
|
||||
override fun onForwardClick(eventId: EventId) {
|
||||
onForwardClickLambda(eventId)
|
||||
override fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean) {
|
||||
onForwardClickLambda(eventId, fromPinnedEvents)
|
||||
}
|
||||
|
||||
override fun onItemDeleted() {
|
||||
|
|
|
|||
|
|
@ -785,7 +785,7 @@ class MediaViewerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - forward hides the bottom sheet and invokes the navigator`() = runTest {
|
||||
val onForwardClickLambda = lambdaRecorder<EventId, Unit> { }
|
||||
val onForwardClickLambda = lambdaRecorder<EventId, Boolean, Unit> { _, _ -> }
|
||||
val navigator = FakeMediaViewerNavigator(
|
||||
onForwardClickLambda = onForwardClickLambda,
|
||||
)
|
||||
|
|
@ -804,7 +804,35 @@ class MediaViewerPresenterTest {
|
|||
initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
onForwardClickLambda.assertions().isCalledOnce()
|
||||
.with(value(AN_EVENT_ID), value(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - forward from pinned events hides the bottom sheet and invokes the navigator`() = runTest {
|
||||
val onForwardClickLambda = lambdaRecorder<EventId, Boolean, Unit> { _, _ -> }
|
||||
val navigator = FakeMediaViewerNavigator(
|
||||
onForwardClickLambda = onForwardClickLambda,
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.PinnedEvents),
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaViewerNavigator = navigator,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onForwardClickLambda.assertions().isCalledOnce()
|
||||
.with(value(AN_EVENT_ID), value(true))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.libraries.mediaviewer.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeMediaGalleryEntryPoint : MediaGalleryEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: MediaGalleryEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.libraries.mediaviewer.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeMediaViewerEntryPoint : MediaViewerEntryPoint {
|
||||
override fun createParamsForAvatar(filename: String, avatarUrl: String) = lambdaError()
|
||||
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: MediaViewerEntryPoint.Params,
|
||||
callback: MediaViewerEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ class DefaultPermissionsPresenter(
|
|||
showDialog = showDialog.value,
|
||||
permissionAlreadyAsked = isAlreadyAsked,
|
||||
permissionAlreadyDenied = isAlreadyDenied,
|
||||
eventSink = { handleEvents(it) }
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.preferences.test"
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.previewutils"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ android {
|
|||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
register("nightly") {
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
|
|
|||
BIN
libraries/push/impl/src/debug/res/raw/message.mp3
Normal file
BIN
libraries/push/impl/src/debug/res/raw/message.mp3
Normal file
Binary file not shown.
|
|
@ -62,7 +62,7 @@ class IgnoredUsersTest(
|
|||
coroutineScope: CoroutineScope,
|
||||
navigator: NotificationTroubleshootNavigator,
|
||||
) {
|
||||
navigator.openIgnoredUsers()
|
||||
navigator.navigateToBlockedUsers()
|
||||
}
|
||||
|
||||
override suspend fun reset() = delegate.reset()
|
||||
|
|
|
|||
BIN
libraries/push/impl/src/nightly/res/raw/message.mp3
Normal file
BIN
libraries/push/impl/src/nightly/res/raw/message.mp3
Normal file
Binary file not shown.
|
|
@ -39,7 +39,7 @@ class IgnoredUsersTestTest {
|
|||
)
|
||||
val openIgnoredUsersResult = lambdaRecorder<Unit> {}
|
||||
val navigator = object : NotificationTroubleshootNavigator {
|
||||
override fun openIgnoredUsers() = openIgnoredUsersResult()
|
||||
override fun navigateToBlockedUsers() = openIgnoredUsersResult()
|
||||
}
|
||||
sut.quickFix(
|
||||
coroutineScope = backgroundScope,
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ interface RoomSelectEntryPoint : FeatureEntryPoint {
|
|||
val mode: RoomSelectMode,
|
||||
)
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: Params,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onRoomSelected(roomIds: List<RoomId>)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.roomselect.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -17,23 +16,18 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
|||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : RoomSelectEntryPoint.NodeBuilder {
|
||||
override fun params(params: RoomSelectEntryPoint.Params): RoomSelectEntryPoint.NodeBuilder {
|
||||
plugins += RoomSelectNode.Inputs(mode = params.mode)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: RoomSelectEntryPoint.Callback): RoomSelectEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<RoomSelectNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: RoomSelectEntryPoint.Params,
|
||||
callback: RoomSelectEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<RoomSelectNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
RoomSelectNode.Inputs(mode = params.mode),
|
||||
callback,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
|
||||
|
|
@ -35,24 +35,15 @@ class RoomSelectNode(
|
|||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val presenter = presenterFactory.create(inputs.mode)
|
||||
|
||||
private val callbacks = plugins.filterIsInstance<RoomSelectEntryPoint.Callback>()
|
||||
|
||||
private fun onDismiss() {
|
||||
callbacks.forEach { it.onCancel() }
|
||||
}
|
||||
|
||||
private fun onSubmit(roomIds: List<RoomId>) {
|
||||
callbacks.forEach { it.onRoomSelected(roomIds) }
|
||||
}
|
||||
private val callback: RoomSelectEntryPoint.Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomSelectView(
|
||||
state = state,
|
||||
onDismiss = ::onDismiss,
|
||||
onSubmit = ::onSubmit,
|
||||
onDismiss = callback::onCancel,
|
||||
onSubmit = callback::onRoomSelected,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class RoomSelectPresenter(
|
|||
query = searchQuery,
|
||||
isSearchActive = isSearchActive,
|
||||
selectedRooms = selectedRooms,
|
||||
eventSink = { handleEvents(it) }
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,10 +42,12 @@ class DefaultRoomSelectEntryPointTest {
|
|||
override fun onCancel() = lambdaError()
|
||||
}
|
||||
val params = RoomSelectEntryPoint.Params(testMode)
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
params = params,
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(RoomSelectNode::class.java)
|
||||
assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode))
|
||||
assertThat(result.plugins).contains(callback)
|
||||
|
|
|
|||
19
libraries/roomselect/test/build.gradle.kts
Normal file
19
libraries/roomselect/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.roomselect.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.roomselect.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.libraries.roomselect.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeRoomSelectEntryPoint : RoomSelectEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
params: RoomSelectEntryPoint.Params,
|
||||
callback: RoomSelectEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -50,6 +50,11 @@ interface SessionStore {
|
|||
*/
|
||||
suspend fun getAllSessions(): List<SessionData>
|
||||
|
||||
/**
|
||||
* Get the number of sessions.
|
||||
*/
|
||||
suspend fun numberOfSessions(): Int
|
||||
|
||||
/**
|
||||
* Get the latest session, or null if no session exists.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -161,6 +161,15 @@ class DatabaseSessionStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun numberOfSessions(): Int {
|
||||
return sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.count()
|
||||
.executeAsOneOrNull()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun sessionsFlow(): Flow<List<SessionData>> {
|
||||
return database.sessionDataQueries.selectAll()
|
||||
.asFlow()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ SELECT * FROM SessionData ORDER BY lastUsageIndex DESC LIMIT 1;
|
|||
selectAll:
|
||||
SELECT * FROM SessionData ORDER BY lastUsageIndex DESC;
|
||||
|
||||
count:
|
||||
SELECT count(*) FROM SessionData;
|
||||
|
||||
selectByUserId:
|
||||
SELECT * FROM SessionData WHERE userId = ?;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class DatabaseSessionStoreTest {
|
|||
|
||||
assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isEqualTo(aSessionData)
|
||||
assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(1)
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -109,6 +110,7 @@ class DatabaseSessionStoreTest {
|
|||
|
||||
assertThat(foundSession).isEqualTo(aSessionData)
|
||||
assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(2)
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -196,12 +198,16 @@ class DatabaseSessionStoreTest {
|
|||
position = 1,
|
||||
lastUsageIndex = 1,
|
||||
)
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1)
|
||||
databaseSessionStore.addSession(secondSessionData.toApiModel())
|
||||
assertThat(awaitItem().size).isEqualTo(2)
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(2)
|
||||
databaseSessionStore.removeSession(aSessionData.userId)
|
||||
assertThat(awaitItem().size).isEqualTo(1)
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1)
|
||||
databaseSessionStore.removeSession(secondSessionData.userId)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ class InMemorySessionStore(
|
|||
return sessionDataListFlow.value
|
||||
}
|
||||
|
||||
override suspend fun numberOfSessions(): Int {
|
||||
return sessionDataListFlow.value.size
|
||||
}
|
||||
|
||||
override suspend fun getLatestSession(): SessionData? {
|
||||
return sessionDataListFlow.value.firstOrNull()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_a11y_add_attachment">"افزودن پیوست"</string>
|
||||
<string name="rich_text_editor_bullet_list">"تغییر وضعیت سیاههٔ گلولهای"</string>
|
||||
<string name="rich_text_editor_close_formatting_options">"بستن گزینههای قالببندی"</string>
|
||||
<string name="rich_text_editor_close_formatting_options">"لغو و بستن قالببندی متن"</string>
|
||||
<string name="rich_text_editor_code_block">"تغییر حالت بلوک کد"</string>
|
||||
<string name="rich_text_editor_composer_caption_placeholder">"افزودن عنوان"</string>
|
||||
<string name="rich_text_editor_composer_encrypted_placeholder">"پیام رمزنگاری شده…"</string>
|
||||
|
|
|
|||
|
|
@ -13,15 +13,14 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface NotificationTroubleShootEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
fun openIgnoredUsers()
|
||||
fun navigateToBlockedUsers()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,14 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface PushHistoryEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
fun navigateTo(roomId: RoomId, eventId: EventId)
|
||||
fun navigateToEvent(roomId: RoomId, eventId: EventId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
package io.element.android.libraries.troubleshoot.api.test
|
||||
|
||||
interface NotificationTroubleshootNavigator {
|
||||
fun openIgnoredUsers()
|
||||
fun navigateToBlockedUsers()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.troubleshoot.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -17,18 +16,11 @@ import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEnt
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationTroubleShootEntryPoint : NotificationTroubleShootEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NotificationTroubleShootEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : NotificationTroubleShootEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: NotificationTroubleShootEntryPoint.Callback): NotificationTroubleShootEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<TroubleshootNotificationsNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: NotificationTroubleShootEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<TroubleshootNotificationsNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
|
||||
|
|
@ -31,20 +31,13 @@ class TroubleshootNotificationsNode(
|
|||
factory: TroubleshootNotificationsPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
NotificationTroubleshootNavigator {
|
||||
private val callback: NotificationTroubleShootEntryPoint.Callback = callback()
|
||||
private val presenter = factory.create(
|
||||
navigator = this,
|
||||
)
|
||||
|
||||
private fun onDone() {
|
||||
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
|
||||
it.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
override fun openIgnoredUsers() {
|
||||
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
|
||||
it.openIgnoredUsers()
|
||||
}
|
||||
override fun navigateToBlockedUsers() {
|
||||
callback.navigateToBlockedUsers()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -53,7 +46,7 @@ class TroubleshootNotificationsNode(
|
|||
val state = presenter.present()
|
||||
TroubleshootNotificationsView(
|
||||
state = state,
|
||||
onBackClick = ::onDone,
|
||||
onBackClick = callback::onDone,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.troubleshoot.impl.history
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -17,18 +16,11 @@ import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHistoryEntryPoint : PushHistoryEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PushHistoryEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : PushHistoryEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: PushHistoryEntryPoint.Callback): PushHistoryEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<PushHistoryNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: PushHistoryEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<PushHistoryNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -31,16 +31,10 @@ class PushHistoryNode(
|
|||
presenterFactory: PushHistoryPresenter.Factory,
|
||||
private val screenTracker: ScreenTracker,
|
||||
) : Node(buildContext, plugins = plugins), PushHistoryNavigator {
|
||||
private fun onDone() {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.onDone()
|
||||
}
|
||||
}
|
||||
private val callback: PushHistoryEntryPoint.Callback = callback()
|
||||
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.navigateTo(roomId, eventId)
|
||||
}
|
||||
callback.navigateToEvent(roomId, eventId)
|
||||
}
|
||||
|
||||
private val presenter = presenterFactory.create(this)
|
||||
|
|
@ -51,7 +45,7 @@ class PushHistoryNode(
|
|||
val state = presenter.present()
|
||||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = ::onDone,
|
||||
onBackClick = callback::onDone,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ class DefaultNotificationTroubleShootEntryPointTest {
|
|||
}
|
||||
val callback = object : NotificationTroubleShootEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun openIgnoredUsers() = lambdaError()
|
||||
override fun navigateToBlockedUsers() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(TroubleshootNotificationsNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ private fun createTroubleshootTestSuite(
|
|||
|
||||
internal fun createTroubleshootNotificationsPresenter(
|
||||
navigator: NotificationTroubleshootNavigator = object : NotificationTroubleshootNavigator {
|
||||
override fun openIgnoredUsers() = lambdaError()
|
||||
override fun navigateToBlockedUsers() = lambdaError()
|
||||
},
|
||||
troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(),
|
||||
): TroubleshootNotificationsPresenter {
|
||||
|
|
|
|||
|
|
@ -46,11 +46,13 @@ class DefaultPushHistoryEntryPointTest {
|
|||
}
|
||||
val callback = object : PushHistoryEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError()
|
||||
override fun navigateToEvent(roomId: RoomId, eventId: EventId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(PushHistoryNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.troubleshoot.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.libraries.troubleshoot.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeNotificationTroubleShootEntryPoint : NotificationTroubleShootEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: NotificationTroubleShootEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -13,5 +13,5 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||
class FakeNotificationTroubleshootNavigator(
|
||||
private val openIgnoredUsersResult: () -> Unit = { lambdaError() },
|
||||
) : NotificationTroubleshootNavigator {
|
||||
override fun openIgnoredUsers() = openIgnoredUsersResult()
|
||||
override fun navigateToBlockedUsers() = openIgnoredUsersResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.libraries.troubleshoot.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakePushHistoryEntryPoint : PushHistoryEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: PushHistoryEntryPoint.Callback,
|
||||
): Node = lambdaError()
|
||||
}
|
||||
|
|
@ -316,6 +316,8 @@
|
|||
<string name="screen_share_open_google_maps">"Отваряне в Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Отваряне в OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Споделяне на това местоположение"</string>
|
||||
<string name="screen_space_settings_leave_space">"Напускане на пространството"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Защита и поверителност"</string>
|
||||
<string name="screen_view_location_title">"Местоположение"</string>
|
||||
<string name="settings_version_number">"Версия: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"bg"</string>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
<string name="action_forgot_password">"Zapomněli jste heslo?"</string>
|
||||
<string name="action_forward">"Přeposlat"</string>
|
||||
<string name="action_go_back">"Přejít zpět"</string>
|
||||
<string name="action_go_to_roles_and_permissions">"Přejít na role a oprávnění"</string>
|
||||
<string name="action_go_to_settings">"Přejít do nastavení"</string>
|
||||
<string name="action_ignore">"Ignorovat"</string>
|
||||
<string name="action_invite">"Pozvat"</string>
|
||||
|
|
@ -178,7 +179,6 @@
|
|||
<string name="common_advanced_settings">"Pokročilá nastavení"</string>
|
||||
<string name="common_an_image">"obrázek"</string>
|
||||
<string name="common_analytics">"Analytika"</string>
|
||||
<string name="common_android_notification_sync_notifications_foreground_service_title">"Načítání oznámení…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Opustili jste místnost"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Byli jste odhlášeni z relace"</string>
|
||||
<string name="common_appearance">"Vzhled"</string>
|
||||
|
|
@ -476,6 +476,9 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
|
||||
<string name="screen_space_list_title">"Prostory"</string>
|
||||
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení."</string>
|
||||
|
|
|
|||
|
|
@ -497,6 +497,8 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"Gofod %1$s"</string>
|
||||
<string name="screen_space_list_title">"Gofodau"</string>
|
||||
<string name="screen_space_settings_leave_space">"Gadael y gofod"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Diogelwch a phreifatrwydd"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Heb anfon y neges oherwydd bod hunaniaeth wedi \'i ddilysu %1$s wedi\'i ailosod."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Heb anfon y neges oherwydd nid yw %1$s wedi gwirio pob dyfais."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Heb anfon y neges oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau."</string>
|
||||
|
|
|
|||
|
|
@ -466,6 +466,8 @@ Er du sikker på, at du vil fortsætte?"</string>
|
|||
<string name="screen_space_list_parent_space">"%1$s gruppe"</string>
|
||||
<string name="screen_space_list_title">"Grupper"</string>
|
||||
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
||||
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder."</string>
|
||||
|
|
|
|||
|
|
@ -420,6 +420,7 @@ Möchtest du wirklich fortfahren?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Begleite mich auf %1$s"</string>
|
||||
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="preference_rageshake">"Heftiges Schütteln um Fehler zu melden"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Bildschirmfoto"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
@ -466,6 +467,9 @@ Möchtest du wirklich fortfahren?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s Space"</string>
|
||||
<string name="screen_space_list_title">"Spaces"</string>
|
||||
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
||||
<string name="screen_space_settings_leave_space">"Space verlassen"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sicherheit & Datenschutz"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast."</string>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@
|
|||
<string name="screen_share_open_google_maps">"Άνοιγμα στο Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Άνοιγμα στο OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Κοινή χρήση αυτής της τοποθεσίας"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Ασφάλεια & απόρρητο"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Το μήνυμα δεν στάλθηκε γιατί έγινε επαναφορά της επαληθευμένης ταυτότητας του χρήστη %1$s."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Το μήνυμα δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει όλες τις συσκευές."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Το μήνυμα δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου."</string>
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ Motivo: %1$s."</string>
|
|||
<string name="screen_share_open_google_maps">"Abrir en Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Abrir en OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Compartir esta ubicación"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Seguridad y privacidad"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensaje no enviado porque la identidad verificada de %1$s fue restablecida."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mensaje no enviado porque %1$s no ha verificado todos los dispositivos."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensaje no enviado porque no has verificado uno o más de tus dispositivos."</string>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
<string name="action_forgot_password">"Kas unustasid salasõna?"</string>
|
||||
<string name="action_forward">"Edasta"</string>
|
||||
<string name="action_go_back">"Tagasi eelmisesse vaatesse"</string>
|
||||
<string name="action_go_to_roles_and_permissions">"Ava „Rollid ja õigused“"</string>
|
||||
<string name="action_go_to_settings">"Ava seadistused"</string>
|
||||
<string name="action_ignore">"Eira"</string>
|
||||
<string name="action_invite">"Kutsu"</string>
|
||||
|
|
@ -420,6 +421,7 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Liitu minuga rakenduses %1$s"</string>
|
||||
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Jutulõng „%1$s“ jututoas"</string>
|
||||
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Ekraanitõmmis"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
@ -467,6 +469,9 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
|
||||
<string name="screen_space_list_title">"Kogukonnad"</string>
|
||||
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
|
||||
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata."</string>
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ Ziur jarraitu nahi duzula?"</string>
|
|||
<string name="screen_share_open_google_maps">"Ireki Google Maps-en"</string>
|
||||
<string name="screen_share_open_osm_maps">"Ireki OpenStreetMap-en"</string>
|
||||
<string name="screen_share_this_location_action">"Partekatu kokapen hau"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurtasuna eta pribatutasuna"</string>
|
||||
<string name="screen_view_location_title">"Kokapena"</string>
|
||||
<string name="settings_version_number">"Bertsioa: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"eu"</string>
|
||||
|
|
|
|||
|
|
@ -383,6 +383,8 @@
|
|||
<string name="screen_space_list_parent_space">"%1$s فضا"</string>
|
||||
<string name="screen_space_list_title">"فضاها"</string>
|
||||
<string name="screen_space_menu_action_members">"دیدن اعضا"</string>
|
||||
<string name="screen_space_settings_leave_space">"ترک فضا"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"امنیت و محرمانگی"</string>
|
||||
<string name="screen_view_location_title">"مکان"</string>
|
||||
<string name="settings_version_number">"نگارش : %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"fa"</string>
|
||||
|
|
|
|||
|
|
@ -467,6 +467,8 @@ Haluatko varmasti jatkaa?"</string>
|
|||
<string name="screen_space_list_parent_space">"%1$s tila"</string>
|
||||
<string name="screen_space_list_title">"Tilat"</string>
|
||||
<string name="screen_space_menu_action_members">"Näytä jäsenet"</string>
|
||||
<string name="screen_space_settings_leave_space">"Poistu tilasta"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Turvallisuus ja yksityisyys"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."</string>
|
||||
|
|
|
|||
|
|
@ -467,6 +467,8 @@ Raison : %1$s."</string>
|
|||
<string name="screen_space_list_parent_space">"Espace %1$s"</string>
|
||||
<string name="screen_space_list_title">"Espaces"</string>
|
||||
<string name="screen_space_menu_action_members">"Voir les membres"</string>
|
||||
<string name="screen_space_settings_leave_space">"Quitter l’espace"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sécurité & confidentialité"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Le message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message non envoyé car vous n’avez pas vérifié tous vos appareils."</string>
|
||||
|
|
|
|||
|
|
@ -465,6 +465,8 @@ Biztos, hogy folytatja?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s tér"</string>
|
||||
<string name="screen_space_list_title">"Terek"</string>
|
||||
<string name="screen_space_settings_leave_space">"Tér elhagyása"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Biztonság és adatvédelem"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte."</string>
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@ Apakah Anda yakin ingin melanjutkan?"</string>
|
|||
<string name="screen_share_open_google_maps">"Buka di Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Buka di OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Bagikan lokasi ini"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Keamanan & privasi"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Pesan tidak terkirim karena identitas terverifikasi %1$s telah diatur ulang."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Pesan tidak terkirim karena %1$s belum memverifikasi semua perangkat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Pesan tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda."</string>
|
||||
|
|
|
|||
|
|
@ -447,6 +447,7 @@ Sei sicuro di voler continuare?"</string>
|
|||
<string name="screen_space_list_description">"Spazi che hai creato o a cui hai aderito."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_title">"Spazi"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sicurezza e privacy"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Messaggio non inviato perché non hai verificato uno o più dispositivi."</string>
|
||||
|
|
|
|||
|
|
@ -440,6 +440,7 @@
|
|||
<string name="screen_space_list_description">"당신이 스페이스를 만들거나 가입했습니다."</string>
|
||||
<string name="screen_space_list_details">"%1$s•%2$s"</string>
|
||||
<string name="screen_space_list_title">"스페이스"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"보안 및 개인정보 보호"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"%1$s의 인증된 신원이 재설정되어 메시지가 전송되지 않았습니다."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"%1$s 이 모든 장치를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"하나 이상의 기기를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다."</string>
|
||||
|
|
|
|||
|
|
@ -465,6 +465,8 @@ Er du sikker på at du vil fortsette?"</string>
|
|||
<string name="screen_space_list_parent_space">"%1$s område"</string>
|
||||
<string name="screen_space_list_title">"Områder"</string>
|
||||
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
|
||||
<string name="screen_space_settings_leave_space">"Forlat område"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhet og personvern"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine."</string>
|
||||
|
|
|
|||
|
|
@ -475,6 +475,8 @@ Czy na pewno chcesz kontynuować?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"Przestrzeń %1$s"</string>
|
||||
<string name="screen_space_list_title">"Przestrzenie"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opuść przestrzeń"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Bezpieczeństwo i prywatność"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Wiadomość nie została wysłana, ponieważ tożsamość %1$s została zresetowana."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Wiadomość nie została wysłana, ponieważ %1$s nie zweryfikował wszystkich urządzeń."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń."</string>
|
||||
|
|
|
|||
|
|
@ -428,6 +428,7 @@ Você tem certeza de que deseja continuar?"</string>
|
|||
<string name="screen_space_list_description">"Os espaços que você criou ou entrou."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_title">"Espaços"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensagem não enviada porque a identidade verificada de %1$s foi redefinida."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"A mensagem não foi enviada porque %1$s não verificou todos os dispositivos."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos."</string>
|
||||
|
|
|
|||
|
|
@ -461,6 +461,8 @@ Tens a certeza de que queres continuar?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"Espaço %1$s"</string>
|
||||
<string name="screen_space_list_title">"Espaços"</string>
|
||||
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensagem não enviada porque a identidade verificada de %1$s foi reposta."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mensagem não enviada porque %1$s não verificou todos os dispositivos."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensagem não enviada porque não verificou um ou mais dos seus dispositivos."</string>
|
||||
|
|
|
|||
|
|
@ -475,6 +475,8 @@ Sunteți sigur că doriți să continuați?"</string>
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"Spațiu %1$s"</string>
|
||||
<string name="screen_space_list_title">"Spații"</string>
|
||||
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Securitate & confidențialitate"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive."</string>
|
||||
|
|
|
|||
|
|
@ -473,6 +473,8 @@
|
|||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
|
||||
<string name="screen_space_list_title">"Пространства"</string>
|
||||
<string name="screen_space_settings_leave_space">"Покинуть пространство"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Безопасность и конфиденциальность"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
<string name="action_forgot_password">"Zabudnuté heslo?"</string>
|
||||
<string name="action_forward">"Preposlať"</string>
|
||||
<string name="action_go_back">"Ísť späť"</string>
|
||||
<string name="action_go_to_roles_and_permissions">"Prejsť na roly a oprávnenia"</string>
|
||||
<string name="action_go_to_settings">"Prejsť na nastavenia"</string>
|
||||
<string name="action_ignore">"Ignorovať"</string>
|
||||
<string name="action_invite">"Pozvať"</string>
|
||||
|
|
@ -178,7 +179,6 @@
|
|||
<string name="common_advanced_settings">"Pokročilé nastavenia"</string>
|
||||
<string name="common_an_image">"obrázok"</string>
|
||||
<string name="common_analytics">"Analytika"</string>
|
||||
<string name="common_android_notification_sync_notifications_foreground_service_title">"Načítavajú sa upozornenia…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Opustili ste miestnosť"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Boli ste odhlásení zo relácie."</string>
|
||||
<string name="common_appearance">"Vzhľad"</string>
|
||||
|
|
@ -478,6 +478,9 @@ Naozaj chcete pokračovať?"</string>
|
|||
<string name="screen_space_list_parent_space">"%1$s priestor"</string>
|
||||
<string name="screen_space_list_title">"Priestory"</string>
|
||||
<string name="screen_space_menu_action_members">"Zobraziť členov"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opustiť priestor"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roly a oprávnenia"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Bezpečnosť a súkromie"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení."</string>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue