Merge branch 'develop' into feature/fga/space_list_join_action
This commit is contained in:
commit
0390fde615
591 changed files with 6155 additions and 2140 deletions
18
libraries/accountselect/api/build.gradle.kts
Normal file
18
libraries/accountselect/api/build.gradle.kts
Normal file
|
|
@ -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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.accountselect.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.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
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onSelectAccount(sessionId: SessionId)
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
35
libraries/accountselect/impl/build.gradle.kts
Normal file
35
libraries/accountselect/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* 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-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.accountselect.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.accountselect.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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 dev.zacsweers.metro.AppScope
|
||||
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
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class AccountSelectNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@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) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
AccountSelectView(
|
||||
state = state,
|
||||
onDismiss = ::onDismiss,
|
||||
onSelectAccount = ::onSelectAccount,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Inject
|
||||
class AccountSelectPresenter(
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<AccountSelectState> {
|
||||
@Composable
|
||||
override fun present(): AccountSelectState {
|
||||
val accounts by produceState(persistentListOf()) {
|
||||
// Do not use sessionStore.sessionsFlow() to not make it change when an account is selected.
|
||||
value = sessionStore.getAllSessions()
|
||||
.map {
|
||||
MatrixUser(
|
||||
userId = UserId(it.userId),
|
||||
displayName = it.userDisplayName,
|
||||
avatarUrl = it.userAvatarUrl,
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
return AccountSelectState(
|
||||
accounts = accounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AccountSelectState(
|
||||
val accounts: ImmutableList<MatrixUser>,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
open class AccountSelectStateProvider : PreviewParameterProvider<AccountSelectState> {
|
||||
override val values: Sequence<AccountSelectState>
|
||||
get() = sequenceOf(
|
||||
anAccountSelectState(),
|
||||
anAccountSelectState(accounts = aMatrixUserList()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun anAccountSelectState(
|
||||
accounts: List<MatrixUser> = listOf(),
|
||||
) = AccountSelectState(
|
||||
accounts = accounts.toPersistentList(),
|
||||
)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSelectView(
|
||||
state: AccountSelectState,
|
||||
onSelectAccount: (SessionId) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(onBack = { onDismiss() })
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(CommonStrings.common_select_account),
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { onDismiss() })
|
||||
},
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
LazyColumn {
|
||||
items(state.accounts, key = { it.userId }) { matrixUser ->
|
||||
Column {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSelectAccount(matrixUser.userId)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStateProvider::class) state: AccountSelectState) = ElementPreview {
|
||||
AccountSelectView(
|
||||
state = state,
|
||||
onSelectAccount = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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.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 dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AccountSelectPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createAccountSelectPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.accounts).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - multiple accounts case`() = runTest {
|
||||
val presenter = createAccountSelectPresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = A_SESSION_ID.value),
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID_2.value,
|
||||
userDisplayName = "Bob",
|
||||
userAvatarUrl = "avatarUrl",
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.accounts).hasSize(2)
|
||||
val firstAccount = initialState.accounts[0]
|
||||
assertThat(firstAccount).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID,
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
)
|
||||
)
|
||||
val secondAccount = initialState.accounts[1]
|
||||
assertThat(secondAccount).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID_2,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createAccountSelectPresenter(
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = AccountSelectPresenter(
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.accountselect.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAccountSelectEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultAccountSelectEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
AccountSelectNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = createAccountSelectPresenter(),
|
||||
)
|
||||
}
|
||||
val callback = object : AccountSelectEntryPoint.Callback {
|
||||
override fun onSelectAccount(sessionId: SessionId) = lambdaError()
|
||||
override fun onCancel() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(AccountSelectNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.architecture.appyx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
|
||||
|
||||
/**
|
||||
* A [ModifierTransitionHandler] that delegates the creation of the modifier to another handler
|
||||
* based on the [NavTarget]. The idea is to allow different transitions for different [NavTarget]s.
|
||||
*/
|
||||
class DelegateTransitionHandler<NavTarget, State>(
|
||||
private val handlerProvider: (NavTarget) -> ModifierTransitionHandler<NavTarget, State>,
|
||||
) : ModifierTransitionHandler<NavTarget, State>() {
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun createModifier(modifier: Modifier, transition: Transition<State>, descriptor: TransitionDescriptor<NavTarget, State>): Modifier {
|
||||
return handlerProvider(descriptor.element).createModifier(modifier, transition, descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <NavTarget, State> rememberDelegateTransitionHandler(
|
||||
handlerProvider: (NavTarget) -> ModifierTransitionHandler<NavTarget, State>,
|
||||
): ModifierTransitionHandler<NavTarget, State> =
|
||||
remember(handlerProvider) { DelegateTransitionHandler(handlerProvider = handlerProvider) }
|
||||
|
|
@ -8,9 +8,18 @@
|
|||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
data class CoroutineDispatchers(
|
||||
val io: CoroutineDispatcher,
|
||||
val computation: CoroutineDispatcher,
|
||||
val main: CoroutineDispatcher,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
val Default = CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s itt: %2$s"</string>
|
||||
<string name="common_date_date_at_time">"%1$s, %2$s"</string>
|
||||
<string name="common_date_this_month">"Ebben a hónapban"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -72,4 +72,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
RoomPreviewHeader(64.dp),
|
||||
RoomPreviewInviter(56.dp),
|
||||
SpaceMember(24.dp),
|
||||
LeaveSpaceRoom(32.dp),
|
||||
|
||||
AccountItem(32.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@
|
|||
<string name="state_event_room_knock_accepted">"%1$s получи достъп до %2$s"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Вие позволихте на %1$s да се присъедини"</string>
|
||||
<string name="state_event_room_knock_by_you">"Вие поискахте да се присъедините"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s отхвърли заявката на %2$s за присъединяване"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Вие отхвърлихте заявката на %1$s за присъединяване"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s отхвърли вашата заявка за присъединяване"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s вече не се интересува от присъединяване"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Вие отменихте заявката си за присъединяване"</string>
|
||||
<string name="state_event_room_leave">"%1$s напусна стаята"</string>
|
||||
<string name="state_event_room_leave_by_you">"Вие напуснахте стаята"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s промени името на стаята на: %2$s"</string>
|
||||
|
|
@ -39,14 +43,19 @@
|
|||
<string name="state_event_room_pinned_events_changed_by_you">"Вие променихте закачените съобщения"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s закачи съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Вие закачихте съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s откачи съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Вие откачихте съобщение"</string>
|
||||
<string name="state_event_room_reject">"%1$s отхвърли поканата"</string>
|
||||
<string name="state_event_room_reject_by_you">"Вие отхвърлихте поканата"</string>
|
||||
<string name="state_event_room_remove">"%1$s премахна %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Вие премахнахте %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s изпрати покана на %2$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Вие изпратихте покана на %1$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s отмени поканата на %2$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Вие отменихте поканата на %1$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s промени темата на: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Вие променихте темата на: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s премахна темата на стаята"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Вие премахнахте темата на стаята"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s направи неизвестна промяна в членството си"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -99,5 +99,13 @@ enum class FeatureFlags(
|
|||
description = "Renders thread messages as a dedicated timeline. Restarting the app is required for this setting to fully take effect.",
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
)
|
||||
),
|
||||
MultiAccount(
|
||||
key = "feature.multi_account",
|
||||
title = "Multi accounts",
|
||||
description = "Allow the application to connect to multiple accounts at the same time." +
|
||||
"\n\nWARNING: this feature is EXPERIMENTAL and UNSTABLE.",
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ import org.maplibre.android.maps.Projection
|
|||
*/
|
||||
@Composable
|
||||
public inline fun rememberCameraPositionState(
|
||||
key: String? = null,
|
||||
crossinline init: CameraPositionState.() -> Unit = {}
|
||||
): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) {
|
||||
): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) {
|
||||
CameraPositionState().apply(init)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
package io.element.android.libraries.maplibre.compose
|
||||
|
||||
import android.content.ComponentCallbacks
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
|
|
@ -235,11 +235,15 @@ private fun MapView.lifecycleObserver(previousState: MutableState<Lifecycle.Even
|
|||
previousState.value = event
|
||||
}
|
||||
|
||||
private fun MapView.componentCallbacks(): ComponentCallbacks =
|
||||
object : ComponentCallbacks {
|
||||
override fun onConfigurationChanged(config: Configuration) {}
|
||||
private fun MapView.componentCallbacks(): ComponentCallbacks2 =
|
||||
object : ComponentCallbacks2 {
|
||||
override fun onConfigurationChanged(config: Configuration) = Unit
|
||||
|
||||
override fun onLowMemory() {
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
// We call the `MapView.onLowMemory` method for any memory trim level
|
||||
this@componentCallbacks.onLowMemory()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,8 @@ public class SymbolState(
|
|||
|
||||
@Composable
|
||||
public fun rememberSymbolState(
|
||||
key: String? = null,
|
||||
position: LatLng = LatLng(0.0, 0.0)
|
||||
): SymbolState = rememberSaveable(key = key, saver = SymbolState.Saver) {
|
||||
): SymbolState = rememberSaveable(saver = SymbolState.Saver) {
|
||||
SymbolState(position)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,16 @@ interface MatrixClient {
|
|||
* Returns the maximum file upload size allowed by the Matrix server.
|
||||
*/
|
||||
suspend fun getMaxFileUploadSize(): Result<Long>
|
||||
|
||||
/**
|
||||
* Returns the list of shared recent emoji reactions for this account.
|
||||
*/
|
||||
suspend fun getRecentEmojis(): Result<List<String>>
|
||||
|
||||
/**
|
||||
* Adds an emoji to the list of recent emoji reactions for this account.
|
||||
*/
|
||||
suspend fun addRecentEmoji(emoji: String): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
sealed class AuthenticationException(message: String) : Exception(message) {
|
||||
class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId)
|
||||
class InvalidServerName(message: String) : AuthenticationException(message)
|
||||
class SlidingSyncVersion(message: String) : AuthenticationException(message)
|
||||
class Oidc(message: String) : AuthenticationException(message)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.libraries.matrix.api.permalink
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -15,13 +16,15 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* This sealed class represents all the permalink cases.
|
||||
* You don't have to instantiate yourself but should use [PermalinkParser] instead.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface PermalinkData {
|
||||
@Parcelize
|
||||
sealed interface PermalinkData : Parcelable {
|
||||
data class RoomLink(
|
||||
val roomIdOrAlias: RoomIdOrAlias,
|
||||
val eventId: EventId? = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.recentemojis
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
class AddRecentEmoji(
|
||||
private val client: MatrixClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
client.addRecentEmoji(emoji)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.recentemojis
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun interface GetRecentEmojis {
|
||||
suspend operator fun invoke(): Result<List<String>>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultGetRecentEmojis(
|
||||
private val client: MatrixClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : GetRecentEmojis {
|
||||
override suspend operator fun invoke(): Result<List<String>> = withContext(dispatchers.io) {
|
||||
client.getRecentEmojis()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,28 +7,32 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
enum class MessageEventType {
|
||||
CALL_ANSWER,
|
||||
CALL_INVITE,
|
||||
CALL_HANGUP,
|
||||
CALL_CANDIDATES,
|
||||
RTC_NOTIFICATION,
|
||||
KEY_VERIFICATION_READY,
|
||||
KEY_VERIFICATION_START,
|
||||
KEY_VERIFICATION_CANCEL,
|
||||
KEY_VERIFICATION_ACCEPT,
|
||||
KEY_VERIFICATION_KEY,
|
||||
KEY_VERIFICATION_MAC,
|
||||
KEY_VERIFICATION_DONE,
|
||||
REACTION,
|
||||
ROOM_ENCRYPTED,
|
||||
ROOM_MESSAGE,
|
||||
ROOM_REDACTION,
|
||||
STICKER,
|
||||
POLL_END,
|
||||
POLL_RESPONSE,
|
||||
POLL_START,
|
||||
UNSTABLE_POLL_END,
|
||||
UNSTABLE_POLL_RESPONSE,
|
||||
UNSTABLE_POLL_START,
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface MessageEventType {
|
||||
data object CallAnswer : MessageEventType
|
||||
data object CallInvite : MessageEventType
|
||||
data object CallHangup : MessageEventType
|
||||
data object CallCandidates : MessageEventType
|
||||
data object RtcNotification : MessageEventType
|
||||
data object KeyVerificationReady : MessageEventType
|
||||
data object KeyVerificationStart : MessageEventType
|
||||
data object KeyVerificationCancel : MessageEventType
|
||||
data object KeyVerificationAccept : MessageEventType
|
||||
data object KeyVerificationKey : MessageEventType
|
||||
data object KeyVerificationMac : MessageEventType
|
||||
data object KeyVerificationDone : MessageEventType
|
||||
data object Reaction : MessageEventType
|
||||
data object RoomEncrypted : MessageEventType
|
||||
data object RoomMessage : MessageEventType
|
||||
data object RoomRedaction : MessageEventType
|
||||
data object Sticker : MessageEventType
|
||||
data object PollEnd : MessageEventType
|
||||
data object PollResponse : MessageEventType
|
||||
data object PollStart : MessageEventType
|
||||
data object UnstablePollEnd : MessageEventType
|
||||
data object UnstablePollResponse : MessageEventType
|
||||
data object UnstablePollStart : MessageEventType
|
||||
data class Other(val type: String) : MessageEventType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
class RoomMembershipObserver {
|
||||
data class RoomMembershipUpdate(
|
||||
val roomId: RoomId,
|
||||
val isSpace: Boolean,
|
||||
val isUserInRoom: Boolean,
|
||||
val change: MembershipChange,
|
||||
)
|
||||
|
|
@ -22,12 +23,23 @@ class RoomMembershipObserver {
|
|||
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
|
||||
val updates = _updates.asSharedFlow()
|
||||
|
||||
suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) {
|
||||
suspend fun notifyUserLeftRoom(
|
||||
roomId: RoomId,
|
||||
isSpace: Boolean,
|
||||
membershipBeforeLeft: CurrentUserMembership,
|
||||
) {
|
||||
val membershipChange = when (membershipBeforeLeft) {
|
||||
CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
|
||||
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
|
||||
else -> MembershipChange.LEFT
|
||||
}
|
||||
_updates.emit(RoomMembershipUpdate(roomId, false, membershipChange))
|
||||
_updates.emit(
|
||||
RoomMembershipUpdate(
|
||||
roomId = roomId,
|
||||
isSpace = isSpace,
|
||||
isUserInRoom = false,
|
||||
change = membershipChange,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation("net.java.dev.jna:jna:5.17.0@aar")
|
||||
implementation("net.java.dev.jna:jna:5.18.0@aar")
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
|
|
|||
|
|
@ -235,7 +235,6 @@ class RustMatrixClient(
|
|||
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
|
||||
MatrixUser(
|
||||
userId = sessionId,
|
||||
// TODO cache for displayName?
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
)
|
||||
|
|
@ -264,6 +263,16 @@ class RustMatrixClient(
|
|||
// Start notification settings
|
||||
notificationSettingsService.start()
|
||||
|
||||
// Update the user profile in the session store if needed
|
||||
sessionStore.getSession(sessionId.value)?.let { sessionData ->
|
||||
_userProfile.emit(
|
||||
MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = sessionData.userDisplayName,
|
||||
avatarUrl = sessionData.userAvatarUrl,
|
||||
)
|
||||
)
|
||||
}
|
||||
// Force a refresh of the profile
|
||||
getUserProfile()
|
||||
}
|
||||
|
|
@ -399,7 +408,15 @@ class RustMatrixClient(
|
|||
}
|
||||
|
||||
override suspend fun getUserProfile(): Result<MatrixUser> = getProfile(sessionId)
|
||||
.onSuccess { _userProfile.tryEmit(it) }
|
||||
.onSuccess { matrixUser ->
|
||||
_userProfile.emit(matrixUser)
|
||||
// Also update our session storage
|
||||
sessionStore.updateUserProfile(
|
||||
sessionId = sessionId.value,
|
||||
displayName = matrixUser.displayName,
|
||||
avatarUrl = matrixUser.avatarUrl,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||
withContext(sessionDispatcher) {
|
||||
|
|
@ -708,6 +725,18 @@ class RustMatrixClient(
|
|||
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
|
||||
}
|
||||
|
||||
override suspend fun addRecentEmoji(emoji: String): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.addRecentEmoji(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRecentEmojis(): Result<List<String>> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.getRecentEmojis().map { it.emoji }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun File.getCacheSize(
|
||||
includeCryptoDb: Boolean = false,
|
||||
): Long = withContext(sessionDispatcher) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.OidcException
|
|||
fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
val message = this.message ?: "Unknown error"
|
||||
return when (this) {
|
||||
is AuthenticationException -> this
|
||||
is ClientBuildException -> when (this) {
|
||||
is ClientBuildException.Generic -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
|
@ -139,6 +140,8 @@ class RustMatrixAuthenticationService(
|
|||
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
||||
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
|
||||
client.login(username, password, "Element X Android", null)
|
||||
// Ensure that the user is not already logged in with the same account
|
||||
ensureNotAlreadyLoggedIn(client)
|
||||
val sessionData = client.session()
|
||||
.toSessionData(
|
||||
isTokenValid = true,
|
||||
|
|
@ -227,17 +230,19 @@ class RustMatrixAuthenticationService(
|
|||
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
||||
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
|
||||
client.loginWithOidcCallback(callbackUrl)
|
||||
|
||||
// Free the pending data since we won't use it to abort the flow anymore
|
||||
pendingOAuthAuthorizationData?.close()
|
||||
pendingOAuthAuthorizationData = null
|
||||
|
||||
// Ensure that the user is not already logged in with the same account
|
||||
ensureNotAlreadyLoggedIn(client)
|
||||
val sessionData = client.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.OIDC,
|
||||
passphrase = pendingPassphrase,
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
|
||||
// Free the pending data since we won't use it to abort the flow anymore
|
||||
pendingOAuthAuthorizationData?.close()
|
||||
pendingOAuthAuthorizationData = null
|
||||
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
|
@ -253,6 +258,21 @@ class RustMatrixAuthenticationService(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(AuthenticationException.AccountAlreadyLoggedIn::class)
|
||||
private suspend fun ensureNotAlreadyLoggedIn(client: Client) {
|
||||
val newUserId = client.userId()
|
||||
val accountAlreadyLoggedIn = sessionStore.getAllSessions().any {
|
||||
it.userId == newUserId
|
||||
}
|
||||
if (accountAlreadyLoggedIn) {
|
||||
// Sign out the client, ignoring any error
|
||||
runCatchingExceptions {
|
||||
client.logout()
|
||||
}
|
||||
throw AuthenticationException.AccountAlreadyLoggedIn(newUserId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
|
||||
|
|
@ -275,7 +295,8 @@ class RustMatrixAuthenticationService(
|
|||
oidcConfiguration = oidcConfiguration,
|
||||
progressListener = progressListener,
|
||||
)
|
||||
|
||||
// Ensure that the user is not already logged in with the same account
|
||||
ensureNotAlreadyLoggedIn(client)
|
||||
val sessionData = client.session()
|
||||
.toSessionData(
|
||||
isTokenValid = true,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ internal fun Session.toSessionData(
|
|||
passphrase = passphrase,
|
||||
sessionPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
// Note: position and lastUsageIndex will be set by the SessionStore when adding the session
|
||||
position = 0,
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
|
||||
internal fun ExternalSession.toSessionData(
|
||||
|
|
@ -55,4 +60,8 @@ internal fun ExternalSession.toSessionData(
|
|||
passphrase = passphrase,
|
||||
sessionPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
position = 0,
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,53 +11,55 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
|
|||
import org.matrix.rustcomponents.sdk.MessageLikeEventType
|
||||
|
||||
fun MessageEventType.map(): MessageLikeEventType = when (this) {
|
||||
MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER
|
||||
MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE
|
||||
MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP
|
||||
MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES
|
||||
MessageEventType.RTC_NOTIFICATION -> MessageLikeEventType.RTC_NOTIFICATION
|
||||
MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
|
||||
MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
|
||||
MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
|
||||
MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT
|
||||
MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY
|
||||
MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC
|
||||
MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE
|
||||
MessageEventType.REACTION -> MessageLikeEventType.REACTION
|
||||
MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED
|
||||
MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE
|
||||
MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION
|
||||
MessageEventType.STICKER -> MessageLikeEventType.STICKER
|
||||
MessageEventType.POLL_END -> MessageLikeEventType.POLL_END
|
||||
MessageEventType.POLL_RESPONSE -> MessageLikeEventType.POLL_RESPONSE
|
||||
MessageEventType.POLL_START -> MessageLikeEventType.POLL_START
|
||||
MessageEventType.UNSTABLE_POLL_END -> MessageLikeEventType.UNSTABLE_POLL_END
|
||||
MessageEventType.UNSTABLE_POLL_RESPONSE -> MessageLikeEventType.UNSTABLE_POLL_RESPONSE
|
||||
MessageEventType.UNSTABLE_POLL_START -> MessageLikeEventType.UNSTABLE_POLL_START
|
||||
MessageEventType.CallAnswer -> MessageLikeEventType.CallAnswer
|
||||
MessageEventType.CallInvite -> MessageLikeEventType.CallInvite
|
||||
MessageEventType.CallHangup -> MessageLikeEventType.CallHangup
|
||||
MessageEventType.CallCandidates -> MessageLikeEventType.CallCandidates
|
||||
MessageEventType.RtcNotification -> MessageLikeEventType.RtcNotification
|
||||
MessageEventType.KeyVerificationReady -> MessageLikeEventType.KeyVerificationReady
|
||||
MessageEventType.KeyVerificationStart -> MessageLikeEventType.KeyVerificationStart
|
||||
MessageEventType.KeyVerificationCancel -> MessageLikeEventType.KeyVerificationCancel
|
||||
MessageEventType.KeyVerificationAccept -> MessageLikeEventType.KeyVerificationAccept
|
||||
MessageEventType.KeyVerificationKey -> MessageLikeEventType.KeyVerificationKey
|
||||
MessageEventType.KeyVerificationMac -> MessageLikeEventType.KeyVerificationMac
|
||||
MessageEventType.KeyVerificationDone -> MessageLikeEventType.KeyVerificationDone
|
||||
MessageEventType.Reaction -> MessageLikeEventType.Reaction
|
||||
MessageEventType.RoomEncrypted -> MessageLikeEventType.RoomEncrypted
|
||||
MessageEventType.RoomMessage -> MessageLikeEventType.RoomMessage
|
||||
MessageEventType.RoomRedaction -> MessageLikeEventType.RoomRedaction
|
||||
MessageEventType.Sticker -> MessageLikeEventType.Sticker
|
||||
MessageEventType.PollEnd -> MessageLikeEventType.PollEnd
|
||||
MessageEventType.PollResponse -> MessageLikeEventType.PollResponse
|
||||
MessageEventType.PollStart -> MessageLikeEventType.PollStart
|
||||
MessageEventType.UnstablePollEnd -> MessageLikeEventType.UnstablePollEnd
|
||||
MessageEventType.UnstablePollResponse -> MessageLikeEventType.UnstablePollResponse
|
||||
MessageEventType.UnstablePollStart -> MessageLikeEventType.UnstablePollStart
|
||||
is MessageEventType.Other -> MessageLikeEventType.Other(type)
|
||||
}
|
||||
|
||||
fun MessageLikeEventType.map(): MessageEventType = when (this) {
|
||||
MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER
|
||||
MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE
|
||||
MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP
|
||||
MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES
|
||||
MessageLikeEventType.RTC_NOTIFICATION -> MessageEventType.RTC_NOTIFICATION
|
||||
MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
|
||||
MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
|
||||
MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL
|
||||
MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT
|
||||
MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY
|
||||
MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC
|
||||
MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE
|
||||
MessageLikeEventType.REACTION -> MessageEventType.REACTION
|
||||
MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED
|
||||
MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE
|
||||
MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION
|
||||
MessageLikeEventType.STICKER -> MessageEventType.STICKER
|
||||
MessageLikeEventType.POLL_END -> MessageEventType.POLL_END
|
||||
MessageLikeEventType.POLL_RESPONSE -> MessageEventType.POLL_RESPONSE
|
||||
MessageLikeEventType.POLL_START -> MessageEventType.POLL_START
|
||||
MessageLikeEventType.UNSTABLE_POLL_END -> MessageEventType.UNSTABLE_POLL_END
|
||||
MessageLikeEventType.UNSTABLE_POLL_RESPONSE -> MessageEventType.UNSTABLE_POLL_RESPONSE
|
||||
MessageLikeEventType.UNSTABLE_POLL_START -> MessageEventType.UNSTABLE_POLL_START
|
||||
MessageLikeEventType.CallAnswer -> MessageEventType.CallAnswer
|
||||
MessageLikeEventType.CallInvite -> MessageEventType.CallInvite
|
||||
MessageLikeEventType.CallHangup -> MessageEventType.CallHangup
|
||||
MessageLikeEventType.CallCandidates -> MessageEventType.CallCandidates
|
||||
MessageLikeEventType.RtcNotification -> MessageEventType.RtcNotification
|
||||
MessageLikeEventType.KeyVerificationReady -> MessageEventType.KeyVerificationReady
|
||||
MessageLikeEventType.KeyVerificationStart -> MessageEventType.KeyVerificationStart
|
||||
MessageLikeEventType.KeyVerificationCancel -> MessageEventType.KeyVerificationCancel
|
||||
MessageLikeEventType.KeyVerificationAccept -> MessageEventType.KeyVerificationAccept
|
||||
MessageLikeEventType.KeyVerificationKey -> MessageEventType.KeyVerificationKey
|
||||
MessageLikeEventType.KeyVerificationMac -> MessageEventType.KeyVerificationMac
|
||||
MessageLikeEventType.KeyVerificationDone -> MessageEventType.KeyVerificationDone
|
||||
MessageLikeEventType.Reaction -> MessageEventType.Reaction
|
||||
MessageLikeEventType.RoomEncrypted -> MessageEventType.RoomEncrypted
|
||||
MessageLikeEventType.RoomMessage -> MessageEventType.RoomMessage
|
||||
MessageLikeEventType.RoomRedaction -> MessageEventType.RoomRedaction
|
||||
MessageLikeEventType.Sticker -> MessageEventType.Sticker
|
||||
MessageLikeEventType.PollEnd -> MessageEventType.PollEnd
|
||||
MessageLikeEventType.PollResponse -> MessageEventType.PollResponse
|
||||
MessageLikeEventType.PollStart -> MessageEventType.PollStart
|
||||
MessageLikeEventType.UnstablePollEnd -> MessageEventType.UnstablePollEnd
|
||||
MessageLikeEventType.UnstablePollResponse -> MessageEventType.UnstablePollResponse
|
||||
MessageLikeEventType.UnstablePollStart -> MessageEventType.UnstablePollStart
|
||||
is MessageLikeEventType.Other -> MessageEventType.Other(v1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,11 @@ class RustBaseRoom(
|
|||
runCatchingExceptions {
|
||||
innerRoom.leave()
|
||||
}.onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft)
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = roomId,
|
||||
isSpace = roomInfoFlow.value.isSpace,
|
||||
membershipBeforeLeft = membershipBeforeLeft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.map
|
||||
|
|
@ -126,6 +127,7 @@ class TimelineEventContentMapper(
|
|||
source = kind.source.map(),
|
||||
)
|
||||
}
|
||||
is MsgLikeKind.Other -> UnknownContent
|
||||
}
|
||||
}
|
||||
is TimelineItemContent.ProfileChange -> {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ class RustMatrixClientFactoryTest {
|
|||
fun TestScope.createRustMatrixClientFactory(
|
||||
baseDirectory: File = File("/base"),
|
||||
cacheDirectory: File = File("/cache"),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
) = RustMatrixClientFactory(
|
||||
baseDirectory = baseDirectory,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
|
|
@ -12,17 +14,24 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.UserProfile
|
||||
import java.io.File
|
||||
|
||||
class RustMatrixClientTest {
|
||||
|
|
@ -51,9 +60,46 @@ class RustMatrixClientTest {
|
|||
client.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieving the UserProfile updates the database`() = runTest {
|
||||
val updateUserProfileResult = lambdaRecorder<String, String?, String?, Unit> { _, _, _ -> }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
),
|
||||
updateUserProfileResult = updateUserProfileResult,
|
||||
)
|
||||
val client = createRustMatrixClient(
|
||||
client = FakeFfiClient(
|
||||
getProfileResult = { userId ->
|
||||
UserProfile(
|
||||
userId = userId,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
)
|
||||
},
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
updateUserProfileResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(A_USER_ID.value),
|
||||
value(A_USER_NAME),
|
||||
value(AN_AVATAR_URL),
|
||||
)
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
private fun TestScope.createRustMatrixClient(
|
||||
client: Client = FakeFfiClient(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
) = RustMatrixClient(
|
||||
innerClient = client,
|
||||
baseDirectory = File(""),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class FakeFfiClient(
|
|||
private val session: Session = aRustSession(),
|
||||
private val clearCachesResult: () -> Unit = { lambdaError() },
|
||||
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
||||
private val getProfileResult: (String) -> UserProfile = { UserProfile(userId = userId, displayName = null, avatarUrl = null) },
|
||||
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
|
||||
private val closeResult: () -> Unit = {},
|
||||
) : Client(NoPointer) {
|
||||
|
|
@ -79,7 +80,7 @@ class FakeFfiClient(
|
|||
}
|
||||
|
||||
override suspend fun getProfile(userId: String): UserProfile {
|
||||
return UserProfile(userId = userId, displayName = null, avatarUrl = null)
|
||||
return getProfileResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,5 @@ 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 suspend fun build() = buildResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,55 +15,55 @@ import org.matrix.rustcomponents.sdk.MessageLikeEventType
|
|||
class MessageEventTypeKtTest {
|
||||
@Test
|
||||
fun `map Rust type should result to correct Kotlin type`() {
|
||||
assertThat(MessageLikeEventType.CALL_ANSWER.map()).isEqualTo(MessageEventType.CALL_ANSWER)
|
||||
assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE)
|
||||
assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP)
|
||||
assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES)
|
||||
assertThat(MessageLikeEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageEventType.RTC_NOTIFICATION)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_ACCEPT)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_KEY)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_MAC)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_DONE)
|
||||
assertThat(MessageLikeEventType.REACTION.map()).isEqualTo(MessageEventType.REACTION)
|
||||
assertThat(MessageLikeEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageEventType.ROOM_ENCRYPTED)
|
||||
assertThat(MessageLikeEventType.ROOM_MESSAGE.map()).isEqualTo(MessageEventType.ROOM_MESSAGE)
|
||||
assertThat(MessageLikeEventType.ROOM_REDACTION.map()).isEqualTo(MessageEventType.ROOM_REDACTION)
|
||||
assertThat(MessageLikeEventType.STICKER.map()).isEqualTo(MessageEventType.STICKER)
|
||||
assertThat(MessageLikeEventType.POLL_END.map()).isEqualTo(MessageEventType.POLL_END)
|
||||
assertThat(MessageLikeEventType.POLL_RESPONSE.map()).isEqualTo(MessageEventType.POLL_RESPONSE)
|
||||
assertThat(MessageLikeEventType.POLL_START.map()).isEqualTo(MessageEventType.POLL_START)
|
||||
assertThat(MessageLikeEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_END)
|
||||
assertThat(MessageLikeEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_RESPONSE)
|
||||
assertThat(MessageLikeEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_START)
|
||||
assertThat(MessageLikeEventType.CallAnswer.map()).isEqualTo(MessageEventType.CallAnswer)
|
||||
assertThat(MessageLikeEventType.CallInvite.map()).isEqualTo(MessageEventType.CallInvite)
|
||||
assertThat(MessageLikeEventType.CallHangup.map()).isEqualTo(MessageEventType.CallHangup)
|
||||
assertThat(MessageLikeEventType.CallCandidates.map()).isEqualTo(MessageEventType.CallCandidates)
|
||||
assertThat(MessageLikeEventType.RtcNotification.map()).isEqualTo(MessageEventType.RtcNotification)
|
||||
assertThat(MessageLikeEventType.KeyVerificationReady.map()).isEqualTo(MessageEventType.KeyVerificationReady)
|
||||
assertThat(MessageLikeEventType.KeyVerificationStart.map()).isEqualTo(MessageEventType.KeyVerificationStart)
|
||||
assertThat(MessageLikeEventType.KeyVerificationCancel.map()).isEqualTo(MessageEventType.KeyVerificationCancel)
|
||||
assertThat(MessageLikeEventType.KeyVerificationAccept.map()).isEqualTo(MessageEventType.KeyVerificationAccept)
|
||||
assertThat(MessageLikeEventType.KeyVerificationKey.map()).isEqualTo(MessageEventType.KeyVerificationKey)
|
||||
assertThat(MessageLikeEventType.KeyVerificationMac.map()).isEqualTo(MessageEventType.KeyVerificationMac)
|
||||
assertThat(MessageLikeEventType.KeyVerificationDone.map()).isEqualTo(MessageEventType.KeyVerificationDone)
|
||||
assertThat(MessageLikeEventType.Reaction.map()).isEqualTo(MessageEventType.Reaction)
|
||||
assertThat(MessageLikeEventType.RoomEncrypted.map()).isEqualTo(MessageEventType.RoomEncrypted)
|
||||
assertThat(MessageLikeEventType.RoomMessage.map()).isEqualTo(MessageEventType.RoomMessage)
|
||||
assertThat(MessageLikeEventType.RoomRedaction.map()).isEqualTo(MessageEventType.RoomRedaction)
|
||||
assertThat(MessageLikeEventType.Sticker.map()).isEqualTo(MessageEventType.Sticker)
|
||||
assertThat(MessageLikeEventType.PollEnd.map()).isEqualTo(MessageEventType.PollEnd)
|
||||
assertThat(MessageLikeEventType.PollResponse.map()).isEqualTo(MessageEventType.PollResponse)
|
||||
assertThat(MessageLikeEventType.PollStart.map()).isEqualTo(MessageEventType.PollStart)
|
||||
assertThat(MessageLikeEventType.UnstablePollEnd.map()).isEqualTo(MessageEventType.UnstablePollEnd)
|
||||
assertThat(MessageLikeEventType.UnstablePollResponse.map()).isEqualTo(MessageEventType.UnstablePollResponse)
|
||||
assertThat(MessageLikeEventType.UnstablePollStart.map()).isEqualTo(MessageEventType.UnstablePollStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map Kotlin type should result to correct Rust type`() {
|
||||
assertThat(MessageEventType.CALL_ANSWER.map()).isEqualTo(MessageLikeEventType.CALL_ANSWER)
|
||||
assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE)
|
||||
assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP)
|
||||
assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES)
|
||||
assertThat(MessageEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageLikeEventType.RTC_NOTIFICATION)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_ACCEPT)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_KEY)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_MAC)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_DONE)
|
||||
assertThat(MessageEventType.REACTION.map()).isEqualTo(MessageLikeEventType.REACTION)
|
||||
assertThat(MessageEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageLikeEventType.ROOM_ENCRYPTED)
|
||||
assertThat(MessageEventType.ROOM_MESSAGE.map()).isEqualTo(MessageLikeEventType.ROOM_MESSAGE)
|
||||
assertThat(MessageEventType.ROOM_REDACTION.map()).isEqualTo(MessageLikeEventType.ROOM_REDACTION)
|
||||
assertThat(MessageEventType.STICKER.map()).isEqualTo(MessageLikeEventType.STICKER)
|
||||
assertThat(MessageEventType.POLL_END.map()).isEqualTo(MessageLikeEventType.POLL_END)
|
||||
assertThat(MessageEventType.POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.POLL_RESPONSE)
|
||||
assertThat(MessageEventType.POLL_START.map()).isEqualTo(MessageLikeEventType.POLL_START)
|
||||
assertThat(MessageEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_END)
|
||||
assertThat(MessageEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_RESPONSE)
|
||||
assertThat(MessageEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_START)
|
||||
assertThat(MessageEventType.CallAnswer.map()).isEqualTo(MessageLikeEventType.CallAnswer)
|
||||
assertThat(MessageEventType.CallInvite.map()).isEqualTo(MessageLikeEventType.CallInvite)
|
||||
assertThat(MessageEventType.CallHangup.map()).isEqualTo(MessageLikeEventType.CallHangup)
|
||||
assertThat(MessageEventType.CallCandidates.map()).isEqualTo(MessageLikeEventType.CallCandidates)
|
||||
assertThat(MessageEventType.RtcNotification.map()).isEqualTo(MessageLikeEventType.RtcNotification)
|
||||
assertThat(MessageEventType.KeyVerificationReady.map()).isEqualTo(MessageLikeEventType.KeyVerificationReady)
|
||||
assertThat(MessageEventType.KeyVerificationStart.map()).isEqualTo(MessageLikeEventType.KeyVerificationStart)
|
||||
assertThat(MessageEventType.KeyVerificationCancel.map()).isEqualTo(MessageLikeEventType.KeyVerificationCancel)
|
||||
assertThat(MessageEventType.KeyVerificationAccept.map()).isEqualTo(MessageLikeEventType.KeyVerificationAccept)
|
||||
assertThat(MessageEventType.KeyVerificationKey.map()).isEqualTo(MessageLikeEventType.KeyVerificationKey)
|
||||
assertThat(MessageEventType.KeyVerificationMac.map()).isEqualTo(MessageLikeEventType.KeyVerificationMac)
|
||||
assertThat(MessageEventType.KeyVerificationDone.map()).isEqualTo(MessageLikeEventType.KeyVerificationDone)
|
||||
assertThat(MessageEventType.Reaction.map()).isEqualTo(MessageLikeEventType.Reaction)
|
||||
assertThat(MessageEventType.RoomEncrypted.map()).isEqualTo(MessageLikeEventType.RoomEncrypted)
|
||||
assertThat(MessageEventType.RoomMessage.map()).isEqualTo(MessageLikeEventType.RoomMessage)
|
||||
assertThat(MessageEventType.RoomRedaction.map()).isEqualTo(MessageLikeEventType.RoomRedaction)
|
||||
assertThat(MessageEventType.Sticker.map()).isEqualTo(MessageLikeEventType.Sticker)
|
||||
assertThat(MessageEventType.PollEnd.map()).isEqualTo(MessageLikeEventType.PollEnd)
|
||||
assertThat(MessageEventType.PollResponse.map()).isEqualTo(MessageLikeEventType.PollResponse)
|
||||
assertThat(MessageEventType.PollStart.map()).isEqualTo(MessageLikeEventType.PollStart)
|
||||
assertThat(MessageEventType.UnstablePollEnd.map()).isEqualTo(MessageLikeEventType.UnstablePollEnd)
|
||||
assertThat(MessageEventType.UnstablePollResponse.map()).isEqualTo(MessageLikeEventType.UnstablePollResponse)
|
||||
assertThat(MessageEventType.UnstablePollStart.map()).isEqualTo(MessageLikeEventType.UnstablePollStart)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class RustBaseRoomTest {
|
|||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isSpace).isFalse()
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
|
||||
}
|
||||
|
|
@ -77,6 +78,7 @@ class RustBaseRoomTest {
|
|||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isSpace).isFalse()
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
|
||||
}
|
||||
|
|
@ -97,6 +99,7 @@ class RustBaseRoomTest {
|
|||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isSpace).isFalse()
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,8 @@ class FakeMatrixClient(
|
|||
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
|
||||
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
|
||||
private val getJoinedRoomIdsResult: () -> Result<Set<RoomId>> = { Result.success(emptySet()) },
|
||||
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
||||
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
||||
) : MatrixClient {
|
||||
var setDisplayNameCalled: Boolean = false
|
||||
private set
|
||||
|
|
@ -349,4 +351,12 @@ class FakeMatrixClient(
|
|||
override suspend fun getMaxFileUploadSize(): Result<Long> {
|
||||
return getMaxUploadSizeResult()
|
||||
}
|
||||
|
||||
override suspend fun addRecentEmoji(emoji: String): Result<Unit> {
|
||||
return addRecentEmojiLambda(emoji)
|
||||
}
|
||||
|
||||
override suspend fun getRecentEmojis(): Result<List<String>> {
|
||||
return getRecentEmojisLambda()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ const val ANOTHER_MESSAGE = "Hello universe!"
|
|||
const val A_CAPTION = "A media caption"
|
||||
const val A_REASON = "A reason"
|
||||
|
||||
const val A_SPACE_NAME = "A space name"
|
||||
|
||||
const val A_REDACTION_REASON = "A redaction reason"
|
||||
|
||||
const val A_HOMESERVER_URL = "matrix.org"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.libraries.mediaupload.api
|
|||
import android.net.Uri
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.flatMapCatching
|
||||
|
|
@ -25,7 +25,7 @@ import timber.log.Timber
|
|||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaSender(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: JoinedRoom,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ 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.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresent
|
|||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaGalleryNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -44,7 +44,7 @@ import io.element.android.libraries.mediaviewer.impl.model.mediaSource
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaGalleryPresenter(
|
||||
@Assisted private val navigator: MediaGalleryNavigator,
|
||||
private val room: BaseRoom,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ 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.Inject
|
||||
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
|
||||
|
|
@ -40,7 +40,7 @@ import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaGalleryFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ package io.element.android.libraries.mediaviewer.impl.gallery.voice
|
|||
import androidx.compose.runtime.Composable
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.IntoMap
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
|
@ -33,7 +33,7 @@ interface VoiceMessagePresenterModule {
|
|||
fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *>
|
||||
}
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class VoiceMessagePresenter(
|
||||
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
|
||||
@Assisted private val item: MediaItem.Voice,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
|
|
@ -33,7 +33,7 @@ import io.element.android.libraries.mediaviewer.impl.model.hasEvent
|
|||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaViewerNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.runtime.snapshotFlow
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
|
@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.androidutils.R as UtilsR
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class MediaViewerPresenter(
|
||||
@Assisted private val inputs: MediaViewerEntryPoint.Params,
|
||||
@Assisted private val navigator: MediaViewerNavigator,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_media_browser_list_loading_media">"Зареждане на медийни файлове…"</string>
|
||||
<string name="screen_media_browser_list_mode_files">"Файлове"</string>
|
||||
<string name="screen_media_browser_list_mode_media">"Медия"</string>
|
||||
<string name="screen_media_browser_title">"Медия и файлове"</string>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
package io.element.android.libraries.oidc.api
|
||||
|
||||
sealed interface OidcAction {
|
||||
data object GoBack : OidcAction
|
||||
data class GoBack(val toUnblock: Boolean = false) : OidcAction
|
||||
data class Success(val url: String) : OidcAction
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class DefaultOidcUrlParser(
|
|||
*/
|
||||
override fun parse(url: String): OidcAction? {
|
||||
if (url.startsWith(oidcRedirectUrlProvider.provide()).not()) return null
|
||||
if (url.contains("error=access_denied")) return OidcAction.GoBack
|
||||
if (url.contains("error=access_denied")) return OidcAction.GoBack()
|
||||
if (url.contains("code=")) return OidcAction.Success(url)
|
||||
|
||||
// Other case not supported, let's crash the app for now
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ class DefaultOidcActionFlowTest {
|
|||
data.add(action)
|
||||
}
|
||||
}
|
||||
sut.post(OidcAction.GoBack)
|
||||
sut.post(OidcAction.GoBack())
|
||||
delay(1)
|
||||
sut.reset()
|
||||
delay(1)
|
||||
assertThat(data).containsExactly(OidcAction.GoBack, null)
|
||||
assertThat(data).containsExactly(OidcAction.GoBack(), null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class DefaultOidcIntentResolverTest {
|
|||
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(OidcAction.GoBack)
|
||||
assertThat(result).isEqualTo(OidcAction.GoBack())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class DefaultOidcUrlParserTest {
|
|||
fun `test cancel url`() {
|
||||
val sut = createDefaultOidcUrlParser()
|
||||
val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO"
|
||||
assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack)
|
||||
assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.google.accompanist.permissions.shouldShowRationale
|
|||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
|
|
@ -37,7 +37,7 @@ import timber.log.Timber
|
|||
|
||||
private val loggerTag = LoggerTag("DefaultPermissionsPresenter")
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DefaultPermissionsPresenter(
|
||||
@Assisted val permission: String,
|
||||
private val permissionsStore: PermissionsStore,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_check_permission_description">"Проверка дали приложението може да показва известия."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_title">"Проверка на разрешенията"</string>
|
||||
</resources>
|
||||
|
|
@ -10,8 +10,8 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -58,7 +58,7 @@ interface NotificationMediaRepo {
|
|||
): Result<File>
|
||||
}
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DefaultNotificationMediaRepo(
|
||||
@CacheDirectory private val cacheDir: File,
|
||||
private val mxcTools: MxcTools,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Обаждане"</string>
|
||||
<string name="notification_channel_listening_for_events">"Слушане за събития"</string>
|
||||
<string name="notification_channel_noisy">"Шумни известия"</string>
|
||||
<string name="notification_channel_silent">"Безшумни известия"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
|
|
@ -10,12 +12,14 @@
|
|||
<item quantity="one">"%d известие"</item>
|
||||
<item quantity="other">"%d известия"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Имате нови съобщения."</string>
|
||||
<string name="notification_inline_reply_failed">"** Неуспешно изпращане - моля, отворете стаята"</string>
|
||||
<string name="notification_invitation_action_join">"Присъединяване"</string>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d покана"</item>
|
||||
<item quantity="other">"%d покани"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Поканиха ви за чат"</string>
|
||||
<string name="notification_mentioned_you_body">"Ви спомена: %1$s"</string>
|
||||
<string name="notification_new_messages">"Нови съобщения"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
|
|
@ -26,8 +30,14 @@
|
|||
<string name="notification_room_action_mark_as_read">"Отбелязване като прочетено"</string>
|
||||
<string name="notification_room_action_quick_reply">"Бърз отговор"</string>
|
||||
<string name="notification_room_invite_body">"Ви покани да се присъедините към стаята"</string>
|
||||
<string name="notification_sender_me">"Аз"</string>
|
||||
<string name="notification_test_push_notification_content">"Преглеждате известието! Кликнете върху мен!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d непрочетено известено съобщение"</item>
|
||||
<item quantity="other">"%d непрочетени известени съобщения"</item>
|
||||
</plurals>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s и %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s в %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s в %2$s и %3$s"</string>
|
||||
|
|
@ -35,5 +45,20 @@
|
|||
<item quantity="one">"%d стая"</item>
|
||||
<item quantity="other">"%d стаи"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_background_sync_android">"Синхронизация на заден план"</string>
|
||||
<string name="push_distributor_firebase_android">"Услуги на Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Не са намерени валидни услуги на Google Play. Известията може да не работят правилно."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Проверка на блокирани потребители"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Преглед на блокираните потребители"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Няма блокирани потребители."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Блокирани потребители"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Получаване на името на текущия доставчик."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Приложението е изградено с поддръжка за: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Проверка дали приложението може да показва известия."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"Известието не е било кликнато."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Не може да се покаже известието."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_success">"Известието беше натиснато!"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_title">"Показване на известие"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_waiting">"Моля, натиснете известието, за да продължите теста."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Грешка: %1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@
|
|||
<string name="push_distributor_background_sync_android">"Synchronizace na pozadí"</string>
|
||||
<string name="push_distributor_firebase_android">"Služby Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Kontrola blokovaných uživatelů"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Zobrazit blokované uživatele"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Žádní uživatelé nejsou blokováni."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Zablokovali jste %1$d uživatele. Nebudete dostávat oznámení od tohoto uživatele."</item>
|
||||
<item quantity="few">"Zablokovali jste %1$d uživatele. Nebudete dostávat oznámení od těchto uživatelů."</item>
|
||||
<item quantity="other">"Zablokovali jste %1$d uživatelů. Nebudete dostávat oznámení od těchto uživatelů."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Blokovaní uživatelé"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Získat název aktuálního poskytovatele."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nebyli vybráni žádní push poskytovatelé."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktuální push poskytovatel: %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Synkronisering i baggrunden"</string>
|
||||
<string name="push_distributor_firebase_android">"Google-tjenester"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Der blev ikke fundet nogen gyldige Google Play-tjenester. Notifikationer fungerer muligvis ikke korrekt."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Kontrollerer blokerede brugere"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Se blokerede brugere"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Ingen brugere er blokeret."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Du har blokeret %1$d bruger. Du vil ikke modtage meddelelser fra denne bruger."</item>
|
||||
<item quantity="other">"Du har blokeret %1$d brugere. Du vil ikke modtage meddelelser fra disse brugere."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Blokerede brugere"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Få navnet på den aktuelle udbyder."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Ingen push-udbydere valgt."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Nuværende push-udbyder: %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Hintergrundsynchronisation"</string>
|
||||
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Überprüfen von gesperrten Nutzern"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Gesperrte Nutzer ansehen"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Keine Nutzer sind gesperrt."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Du hast %1$d Nutzer gesperrt. Du wirst für diesen Nutzer keine Benachrichtigungen erhalten."</item>
|
||||
<item quantity="other">"Du hast %1$d Nutzer gesperrt. Du wirst für diese Nutzer keine Benachrichtigungen erhalten."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Gesperrte Nutzer"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Ermittele den Namen des aktuellen Anbieters."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Kein Dienst für Push-Benachrichtigungen ausgewählt."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktueller Push-Dienst: %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Sünkroniseerimine taustal"</string>
|
||||
<string name="push_distributor_firebase_android">"Google\'i teenused"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Google Play Services taustateenust ei leidu. Teavitused ei pruugi toimida korrektselt."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Kontrollin blokeeritud kasutajaid"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Vaata blokeeritud kasutajaid"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Sa pole ühtegi kasutajat blokeerinud."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Sa oled blokeerinud %1$d kasutaja. Sa ei saa tema kohta teavitusi"</item>
|
||||
<item quantity="other">"Sa oled blokeerinud %1$d kasutajat. Sa ei saa tema kohta teavitusi"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Blokeeritud kasutajad"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Vali hetkel kasutatava tõuketeenuste pakkuja nimi."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Tõuketeenuste pakkujaid pole valitud."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Hetkel kasutatav tõuketeenuste pakkuja: %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Synchronisation en arrière-plan"</string>
|
||||
<string name="push_distributor_firebase_android">"Services Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Aucun service Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Vérification des utilisateurs bloqués"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Voir les utilisateurs bloqués"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Aucun utilisateur n’est bloqué."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Vous avez bloqué %1$d utilisateur. Vous ne recevrez pas de notifications pour cet utilisateur."</item>
|
||||
<item quantity="other">"Vous avez bloqué %1$d utilisateurs. Vous ne recevrez pas de notifications pour ces utilisateurs."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Utilisateurs bloqués"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Obtenir le nom du fournisseur de Push actuel."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Aucun fournisseur de Push n’est sélectionné."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Fournisseur de Push actuel : %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Háttérszinkronizálás"</string>
|
||||
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"Letiltott felhasználók ellenőrzése"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Letiltott felhasználók megtekintése"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Nincs felhasználó letiltva."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Letiltotta %1$d felhasználót. Nem fog értesítéseket kapni erről a felhasználóról."</item>
|
||||
<item quantity="other">"Letiltott %1$d felhasználót. Nem fog értesítéseket kapni ezekről a felhasználókról."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Letiltott felhasználók"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"A jelenlegi szolgáltató nevének lekérdezése."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nincs kiválasztva leküldéses értesítési szolgáltató."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Jelenlegi leküldéses értesítési szolgáltató: %1$s."</string>
|
||||
|
|
@ -61,8 +69,8 @@
|
|||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Győződjön meg arról, hogy az alkalmazás legalább egy leküldéses értesítési szolgáltatóval rendelkezik."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nem található leküldéses értesítési szolgáltató."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"%1$d leküldéses szolgáltató találva: %2$s"</item>
|
||||
<item quantity="other">"%1$d leküldéses szolgáltató találva: %2$s"</item>
|
||||
<item quantity="one">"%1$d leküldéses értesítési szolgáltató találva: %2$s"</item>
|
||||
<item quantity="other">"%1$d leküldéses értesítési szolgáltató találva: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Az alkalmazás úgy lett összeállítva, hogy támogatja a következőket: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Leküldéses értesítési szolgáltatók észlelése"</string>
|
||||
|
|
@ -78,5 +86,5 @@
|
|||
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Hiba, nem lehet tesztelni a leküldéses értesítést."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Hiba, időtúllépés a leküldéses értesítésre való várakozás során."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_success">"A leküldéses értesítés folyamata %1$d ezredmásodpercig tartott."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_title">"Tesztelje a leküldéses értesítés folyamatát"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_title">"Leküldéses értesítés folyamatának tesztelése"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
<string name="push_distributor_background_sync_android">"Sincronização em segundo plano"</string>
|
||||
<string name="push_distributor_firebase_android">"Serviços do Google Play"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Nenhuns Serviços do Google Play válidos encontrados. As notificações poderão não funcionar devidamente."</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_description">"A verificar utilizadores bloqueados"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"Ver utilizadores bloqueados"</string>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_result_none">"Sem utilizadores bloqueados."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="one">"Bloqueaste %1$d utilizador. Não receberás notificações dele."</item>
|
||||
<item quantity="other">"Bloqueaste %1$d utilizadores. Não receberás notificações deles."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Utilizadores bloqueados"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Obtém o nome do fornecedor atual."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nenhum fornecedor de envio selecionado."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Fornecedor de envio atual: %1$s."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Уверете се, че Firebase е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase не е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Проверка на Firebase"</string>
|
||||
</resources>
|
||||
|
|
@ -13,7 +13,7 @@ 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.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
|||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class RoomSelectNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
|
|
@ -27,7 +27,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class RoomSelectPresenter(
|
||||
@Assisted private val mode: RoomSelectMode,
|
||||
private val dataSource: RoomSelectSearchDataSource,
|
||||
|
|
|
|||
|
|
@ -39,4 +39,12 @@ data class SessionData(
|
|||
val sessionPath: String,
|
||||
/** The path to the cache data stored for the session in the filesystem. */
|
||||
val cachePath: String,
|
||||
/** The position, to be able to order account. */
|
||||
val position: Long,
|
||||
/** The index of the last date of session usage. */
|
||||
val lastUsageIndex: Long,
|
||||
/** The optional display name of the user. */
|
||||
val userDisplayName: String?,
|
||||
/** The optional avatar URL of the user. */
|
||||
val userAvatarUrl: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,22 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface SessionStore {
|
||||
/**
|
||||
* A flow emitting the current logged in state.
|
||||
* If there is at least one session, the state is [LoggedInState.LoggedIn] with the latest used session.
|
||||
* If there is no session, the state is [LoggedInState.NotLoggedIn].
|
||||
*/
|
||||
fun loggedInStateFlow(): Flow<LoggedInState>
|
||||
|
||||
/**
|
||||
* Return a flow of all sessions ordered by last usage descending.
|
||||
*/
|
||||
fun sessionsFlow(): Flow<List<SessionData>>
|
||||
|
||||
/**
|
||||
* Add a new session. If other sessions exist, the new one will be set as the latest used one, and
|
||||
* the added session position will be set to a value higher than the other session positions.
|
||||
*/
|
||||
suspend fun addSession(sessionData: SessionData)
|
||||
|
||||
/**
|
||||
|
|
@ -20,9 +34,35 @@ interface SessionStore {
|
|||
* No op if userId is not found in DB.
|
||||
*/
|
||||
suspend fun updateData(sessionData: SessionData)
|
||||
|
||||
/**
|
||||
* Update the user profile info of the session matching the userId.
|
||||
*/
|
||||
suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?)
|
||||
|
||||
/**
|
||||
* Get the session data matching the userId, or null if not found.
|
||||
*/
|
||||
suspend fun getSession(sessionId: String): SessionData?
|
||||
|
||||
/**
|
||||
* Get all sessions ordered by last usage descending.
|
||||
*/
|
||||
suspend fun getAllSessions(): List<SessionData>
|
||||
|
||||
/**
|
||||
* Get the latest session, or null if no session exists.
|
||||
*/
|
||||
suspend fun getLatestSession(): SessionData?
|
||||
|
||||
/**
|
||||
* Set the session with [sessionId] as the latest used one.
|
||||
*/
|
||||
suspend fun setLatestSession(sessionId: String)
|
||||
|
||||
/**
|
||||
* Remove the session matching the sessionId.
|
||||
*/
|
||||
suspend fun removeSession(sessionId: String)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ dependencies {
|
|||
sqldelight {
|
||||
databases {
|
||||
create("SessionDatabase") {
|
||||
// https://cashapp.github.io/sqldelight/2.0.0/android_sqlite/migrations/
|
||||
// https://sqldelight.github.io/sqldelight/2.1.0/android_sqlite/migrations/
|
||||
// To generate a .db file from your latest schema, run this task
|
||||
// ./gradlew generateDebugSessionDatabaseSchema
|
||||
// Test migration by running
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class DatabaseSessionStore(
|
|||
private val sessionDataMutex = Mutex()
|
||||
|
||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||
return database.sessionDataQueries.selectFirst()
|
||||
return database.sessionDataQueries.selectLatest()
|
||||
.asFlow()
|
||||
.mapToOneOrNull(dispatchers.io)
|
||||
.map {
|
||||
|
|
@ -51,7 +51,17 @@ class DatabaseSessionStore(
|
|||
|
||||
override suspend fun addSession(sessionData: SessionData) {
|
||||
sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
|
||||
val lastUsageIndex = getLastUsageIndex()
|
||||
database.sessionDataQueries.insertSessionData(
|
||||
sessionData
|
||||
.copy(
|
||||
// position value does not really matter, so just use lastUsageIndex + 1 to ensure that
|
||||
// the value is always greater than value of any existing account
|
||||
position = lastUsageIndex + 1,
|
||||
lastUsageIndex = lastUsageIndex + 1,
|
||||
)
|
||||
.toDbModel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,18 +75,71 @@ class DatabaseSessionStore(
|
|||
Timber.e("User ${sessionData.userId} not found in session database")
|
||||
return
|
||||
}
|
||||
// Copy new data from SDK, but keep login timestamp
|
||||
// Copy new data from SDK, but keep application data
|
||||
database.sessionDataQueries.updateSession(
|
||||
sessionData.copy(
|
||||
loginTimestamp = result.loginTimestamp,
|
||||
position = result.position,
|
||||
lastUsageIndex = result.lastUsageIndex,
|
||||
userDisplayName = result.userDisplayName,
|
||||
userAvatarUrl = result.userAvatarUrl,
|
||||
).toDbModel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) {
|
||||
sessionDataMutex.withLock {
|
||||
val result = database.sessionDataQueries.selectByUserId(sessionId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
if (result == null) {
|
||||
Timber.e("User $sessionId not found in session database")
|
||||
return
|
||||
}
|
||||
database.sessionDataQueries.updateSession(
|
||||
result.copy(
|
||||
userDisplayName = displayName,
|
||||
userAvatarUrl = avatarUrl,
|
||||
).toDbModel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setLatestSession(sessionId: String) {
|
||||
val latestSession = getLatestSession()
|
||||
if (latestSession?.userId == sessionId) {
|
||||
// Already the latest session
|
||||
return
|
||||
}
|
||||
val lastUsageIndex = latestSession?.lastUsageIndex ?: 0
|
||||
val result = database.sessionDataQueries.selectByUserId(sessionId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
if (result == null) {
|
||||
Timber.e("User $sessionId not found in session database")
|
||||
return
|
||||
}
|
||||
sessionDataMutex.withLock {
|
||||
// Update lastUsageIndex of the session
|
||||
database.sessionDataQueries.updateSession(
|
||||
result.copy(
|
||||
lastUsageIndex = lastUsageIndex + 1,
|
||||
).toDbModel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastUsageIndex(): Long {
|
||||
return database.sessionDataQueries.selectLatest()
|
||||
.executeAsOneOrNull()
|
||||
?.lastUsageIndex
|
||||
?: -1L
|
||||
}
|
||||
|
||||
override suspend fun getLatestSession(): SessionData? {
|
||||
return sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.selectFirst()
|
||||
database.sessionDataQueries.selectLatest()
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||
passphrase = passphrase,
|
||||
sessionPath = sessionPath,
|
||||
cachePath = cachePath,
|
||||
position = position,
|
||||
lastUsageIndex = lastUsageIndex,
|
||||
userDisplayName = userDisplayName,
|
||||
userAvatarUrl = userAvatarUrl,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -45,5 +49,9 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||
passphrase = passphrase,
|
||||
sessionPath = sessionPath,
|
||||
cachePath = cachePath,
|
||||
position = position,
|
||||
lastUsageIndex = lastUsageIndex,
|
||||
userDisplayName = userDisplayName,
|
||||
userAvatarUrl = userAvatarUrl,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -27,15 +27,25 @@ CREATE TABLE SessionData (
|
|||
-- added in version 6
|
||||
sessionPath TEXT NOT NULL DEFAULT "",
|
||||
-- added in version 9
|
||||
cachePath TEXT NOT NULL DEFAULT ""
|
||||
cachePath TEXT NOT NULL DEFAULT "",
|
||||
-- added in version 10
|
||||
-- position, to be able to sort account by session creation date
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
-- index of the last usage session. Each time the current session change, the index of the current
|
||||
-- session is incremented to the max value + 1 so it becomes the current session
|
||||
lastUsageIndex INTEGER NOT NULL DEFAULT 0,
|
||||
-- user display name
|
||||
userDisplayName TEXT,
|
||||
-- user avatar url
|
||||
userAvatarUrl TEXT
|
||||
);
|
||||
|
||||
|
||||
selectFirst:
|
||||
SELECT * FROM SessionData LIMIT 1;
|
||||
selectLatest:
|
||||
SELECT * FROM SessionData ORDER BY lastUsageIndex DESC LIMIT 1;
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM SessionData;
|
||||
SELECT * FROM SessionData ORDER BY lastUsageIndex DESC;
|
||||
|
||||
selectByUserId:
|
||||
SELECT * FROM SessionData WHERE userId = ?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
-- Migrate DB from version 9
|
||||
-- Add position to be able to sort account by session creation date
|
||||
-- Add lastUsageIndex so we can restore the last session and switch to another one
|
||||
-- Add display name and avatar url of the user so that we can display a list of accounts.
|
||||
|
||||
ALTER TABLE SessionData ADD COLUMN position INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE SessionData ADD COLUMN lastUsageIndex INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE SessionData ADD COLUMN userDisplayName TEXT;
|
||||
ALTER TABLE SessionData ADD COLUMN userAvatarUrl TEXT;
|
||||
|
|
@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.session.SessionData
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -45,11 +46,11 @@ class DatabaseSessionStoreTest {
|
|||
|
||||
@Test
|
||||
fun `addSession persists the SessionData into the DB`() = runTest {
|
||||
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isNull()
|
||||
assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isNull()
|
||||
|
||||
databaseSessionStore.addSession(aSessionData.toApiModel())
|
||||
|
||||
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData)
|
||||
assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isEqualTo(aSessionData)
|
||||
assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(1)
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +60,12 @@ class DatabaseSessionStoreTest {
|
|||
assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn)
|
||||
databaseSessionStore.addSession(aSessionData.toApiModel())
|
||||
assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true))
|
||||
// TODO add more sessions in multi-account PR.
|
||||
// Add a second session
|
||||
databaseSessionStore.addSession(aSessionData.copy(userId = "otherUserId").toApiModel())
|
||||
assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = "otherUserId", isTokenValid = true))
|
||||
// Remove the second session
|
||||
databaseSessionStore.removeSession("otherUserId")
|
||||
assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true))
|
||||
// Remove the first session
|
||||
databaseSessionStore.removeSession(aSessionData.userId)
|
||||
assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn)
|
||||
|
|
@ -124,7 +130,83 @@ class DatabaseSessionStoreTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `update session update all fields except loginTimestamp`() = runTest {
|
||||
fun `updateUserProfile does nothing if the session is not found`() = runTest {
|
||||
databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl")
|
||||
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateUserProfile update the data`() = runTest {
|
||||
database.sessionDataQueries.insertSessionData(aSessionData)
|
||||
databaseSessionStore.updateUserProfile(aSessionData.userId, "userDisplayName", "userAvatarUrl")
|
||||
val updatedSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne()
|
||||
assertThat(updatedSession.userDisplayName).isEqualTo("userDisplayName")
|
||||
assertThat(updatedSession.userAvatarUrl).isEqualTo("userAvatarUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLatestSession is no op when the session is already the latest session`() = runTest {
|
||||
database.sessionDataQueries.insertSessionData(aSessionData)
|
||||
val session = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne()
|
||||
assertThat(session.lastUsageIndex).isEqualTo(0)
|
||||
assertThat(session.position).isEqualTo(0)
|
||||
databaseSessionStore.setLatestSession(aSessionData.userId)
|
||||
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne().lastUsageIndex).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLatestSession is no op when the session is not found`() = runTest {
|
||||
databaseSessionStore.setLatestSession(aSessionData.userId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi session test`() = runTest {
|
||||
databaseSessionStore.addSession(aSessionData.toApiModel())
|
||||
val session = databaseSessionStore.getSession(aSessionData.userId)!!
|
||||
assertThat(session.lastUsageIndex).isEqualTo(0)
|
||||
assertThat(session.position).isEqualTo(0)
|
||||
val secondSessionData = aSessionData.copy(
|
||||
userId = "otherUserId",
|
||||
position = 1,
|
||||
lastUsageIndex = 1,
|
||||
)
|
||||
databaseSessionStore.addSession(secondSessionData.toApiModel())
|
||||
val secondSession = database.sessionDataQueries.selectByUserId(secondSessionData.userId).executeAsOne()
|
||||
assertThat(secondSession.lastUsageIndex).isEqualTo(1)
|
||||
assertThat(secondSession.position).isEqualTo(1)
|
||||
// Set the first session as the latest
|
||||
databaseSessionStore.setLatestSession(aSessionData.userId)
|
||||
val firstSession = database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOne()
|
||||
assertThat(firstSession.lastUsageIndex).isEqualTo(2)
|
||||
assertThat(firstSession.position).isEqualTo(0)
|
||||
// Check that the second session has not been altered
|
||||
val secondSession2 = database.sessionDataQueries.selectByUserId(secondSessionData.userId).executeAsOne()
|
||||
assertThat(secondSession2.lastUsageIndex).isEqualTo(1)
|
||||
assertThat(secondSession2.position).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test sessionsFlow()`() = runTest {
|
||||
databaseSessionStore.sessionsFlow().test {
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
databaseSessionStore.addSession(aSessionData.toApiModel())
|
||||
assertThat(awaitItem().size).isEqualTo(1)
|
||||
val secondSessionData = aSessionData.copy(
|
||||
userId = "otherUserId",
|
||||
position = 1,
|
||||
lastUsageIndex = 1,
|
||||
)
|
||||
databaseSessionStore.addSession(secondSessionData.toApiModel())
|
||||
assertThat(awaitItem().size).isEqualTo(2)
|
||||
databaseSessionStore.removeSession(aSessionData.userId)
|
||||
assertThat(awaitItem().size).isEqualTo(1)
|
||||
databaseSessionStore.removeSession(secondSessionData.userId)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update session update all fields except info used by the application`() = runTest {
|
||||
val firstSessionData = SessionData(
|
||||
userId = "userId",
|
||||
deviceId = "deviceId",
|
||||
|
|
@ -139,6 +221,10 @@ class DatabaseSessionStoreTest {
|
|||
passphrase = "aPassphrase",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
position = 0,
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = "userDisplayName",
|
||||
userAvatarUrl = "userAvatarUrl",
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userId",
|
||||
|
|
@ -152,8 +238,12 @@ class DatabaseSessionStoreTest {
|
|||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
passphrase = "aPassphraseAltered",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
sessionPath = "sessionPathAltered",
|
||||
cachePath = "cachePathAltered",
|
||||
position = 1,
|
||||
lastUsageIndex = 1,
|
||||
userDisplayName = "userDisplayNameAltered",
|
||||
userAvatarUrl = "userAvatarUrlAltered",
|
||||
)
|
||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||
|
|
@ -174,6 +264,11 @@ class DatabaseSessionStoreTest {
|
|||
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||
assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
|
||||
// Check that application data have not been altered
|
||||
assertThat(alteredSession.position).isEqualTo(firstSessionData.position)
|
||||
assertThat(alteredSession.lastUsageIndex).isEqualTo(firstSessionData.lastUsageIndex)
|
||||
assertThat(alteredSession.userDisplayName).isEqualTo(firstSessionData.userDisplayName)
|
||||
assertThat(alteredSession.userAvatarUrl).isEqualTo(firstSessionData.userAvatarUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -188,10 +283,14 @@ class DatabaseSessionStoreTest {
|
|||
loginTimestamp = 1,
|
||||
oidcData = "aOidcData",
|
||||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
loginType = LoginType.PASSWORD.name,
|
||||
passphrase = "aPassphrase",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
position = 0,
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = "userDisplayName",
|
||||
userAvatarUrl = "userAvatarUrl",
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userIdUnknown",
|
||||
|
|
@ -203,10 +302,14 @@ class DatabaseSessionStoreTest {
|
|||
loginTimestamp = 2,
|
||||
oidcData = "aOidcDataAltered",
|
||||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
loginType = LoginType.PASSWORD.name,
|
||||
passphrase = "aPassphraseAltered",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
sessionPath = "sessionPathAltered",
|
||||
cachePath = "cachePathAltered",
|
||||
position = 1,
|
||||
lastUsageIndex = 1,
|
||||
userDisplayName = "userDisplayNameAltered",
|
||||
userAvatarUrl = "userAvatarUrlAltered",
|
||||
)
|
||||
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
||||
|
||||
|
|
@ -216,14 +319,6 @@ class DatabaseSessionStoreTest {
|
|||
// Get the session and check that it has not been altered
|
||||
val notAlteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel()
|
||||
|
||||
assertThat(notAlteredSession.userId).isEqualTo(firstSessionData.userId)
|
||||
assertThat(notAlteredSession.deviceId).isEqualTo(firstSessionData.deviceId)
|
||||
assertThat(notAlteredSession.accessToken).isEqualTo(firstSessionData.accessToken)
|
||||
assertThat(notAlteredSession.refreshToken).isEqualTo(firstSessionData.refreshToken)
|
||||
assertThat(notAlteredSession.homeserverUrl).isEqualTo(firstSessionData.homeserverUrl)
|
||||
assertThat(notAlteredSession.slidingSyncProxy).isEqualTo(firstSessionData.slidingSyncProxy)
|
||||
assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||
assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData)
|
||||
assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase)
|
||||
assertThat(notAlteredSession).isEqualTo(firstSessionData)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,8 @@ internal fun aSessionData() = SessionData(
|
|||
passphrase = null,
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
position = 0,
|
||||
lastUsageIndex = 0,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import kotlinx.coroutines.flow.map
|
|||
|
||||
class InMemorySessionStore(
|
||||
initialList: List<SessionData> = emptyList(),
|
||||
private val updateUserProfileResult: (String, String?, String?) -> Unit = { _, _, _ -> error("Not implemented") },
|
||||
private val setLatestSessionResult: (String) -> Unit = { error("Not implemented") },
|
||||
) : SessionStore {
|
||||
private val sessionDataListFlow = MutableStateFlow(initialList)
|
||||
|
||||
|
|
@ -53,6 +55,10 @@ class InMemorySessionStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) {
|
||||
updateUserProfileResult(sessionId, displayName, avatarUrl)
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionId: String): SessionData? {
|
||||
return sessionDataListFlow.value.firstOrNull { it.userId == sessionId }
|
||||
}
|
||||
|
|
@ -65,6 +71,10 @@ class InMemorySessionStore(
|
|||
return sessionDataListFlow.value.firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun setLatestSession(sessionId: String) {
|
||||
setLatestSessionResult(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun removeSession(sessionId: String) {
|
||||
val currentList = sessionDataListFlow.value.toMutableList()
|
||||
currentList.removeAll { it.userId == sessionId }
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ fun aSessionData(
|
|||
cachePath: String = "/a/path/to/a/cache",
|
||||
accessToken: String = "anAccessToken",
|
||||
refreshToken: String? = "aRefreshToken",
|
||||
): SessionData {
|
||||
position: Long = 0,
|
||||
lastUsageIndex: Long = 0,
|
||||
userDisplayName: String? = null,
|
||||
userAvatarUrl: String? = null,
|
||||
): SessionData {
|
||||
return SessionData(
|
||||
userId = sessionId,
|
||||
deviceId = deviceId,
|
||||
|
|
@ -33,5 +37,9 @@ fun aSessionData(
|
|||
passphrase = null,
|
||||
sessionPath = sessionPath,
|
||||
cachePath = cachePath,
|
||||
position = position,
|
||||
lastUsageIndex = lastUsageIndex,
|
||||
userDisplayName = userDisplayName,
|
||||
userAvatarUrl = userAvatarUrl,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_close_formatting_options">"Отказ и затваряне на форматирането на текст"</string>
|
||||
<string name="rich_text_editor_code_block">"Превключване на кодов блок"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Съобщение…"</string>
|
||||
<string name="rich_text_editor_create_link">"Създаване на връзка"</string>
|
||||
<string name="rich_text_editor_edit_link">"Редактиране на връзката"</string>
|
||||
|
|
@ -8,8 +10,15 @@
|
|||
<string name="rich_text_editor_format_italic">"Прилагане на курсив формат"</string>
|
||||
<string name="rich_text_editor_format_strikethrough">"Прилагане на зачеркнат формат"</string>
|
||||
<string name="rich_text_editor_format_underline">"Прилагане на формат за подчертаване"</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">"Превключване на режим на цял екран"</string>
|
||||
<string name="rich_text_editor_indent">"Отстъп навътре"</string>
|
||||
<string name="rich_text_editor_inline_code">"Прилагане на формат на вграден код"</string>
|
||||
<string name="rich_text_editor_link">"Задаване на връзка"</string>
|
||||
<string name="rich_text_editor_numbered_list">"Превключване на номериран списък"</string>
|
||||
<string name="rich_text_editor_open_compose_options">"Отваряне на опциите за съставяне"</string>
|
||||
<string name="rich_text_editor_quote">"Превключване на цитат"</string>
|
||||
<string name="rich_text_editor_remove_link">"Премахване на връзката"</string>
|
||||
<string name="rich_text_editor_unindent">"Отстъп навън"</string>
|
||||
<string name="rich_text_editor_url_placeholder">"Връзка"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Задръжте, за записване"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface PushHistoryEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
|
@ -25,6 +24,6 @@ interface PushHistoryEntryPoint : FeatureEntryPoint {
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId)
|
||||
fun navigateTo(roomId: RoomId, eventId: EventId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.troubleshoot.api.test.NotificationTroublesho
|
|||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class TroubleshootNotificationsNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class TroubleshootNotificationsPresenter(
|
||||
@Assisted private val navigator: NotificationTroubleshootNavigator,
|
||||
private val troubleshootTestSuite: TroubleshootTestSuite,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface PushHistoryEvents {
|
||||
data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvents
|
||||
data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvents
|
||||
data class NavigateTo(val sessionId: SessionId, val roomId: RoomId, val eventId: EventId) : PushHistoryEvents
|
||||
data object ClearDialog : PushHistoryEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,36 +14,37 @@ 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.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class PushHistoryNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: PushHistoryPresenter,
|
||||
presenterFactory: PushHistoryPresenter.Factory,
|
||||
private val screenTracker: ScreenTracker,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(buildContext, plugins = plugins), PushHistoryNavigator {
|
||||
private fun onDone() {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.onItemClick(sessionId, roomId, eventId)
|
||||
it.navigateTo(roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private val presenter = presenterFactory.create(this)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot)
|
||||
|
|
@ -51,7 +52,6 @@ class PushHistoryNode(
|
|||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = ::onDone,
|
||||
onItemClick = ::onItemClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,36 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
fun interface PushHistoryNavigator {
|
||||
fun navigateTo(roomId: RoomId, eventId: EventId)
|
||||
}
|
||||
|
||||
@AssistedInject
|
||||
class PushHistoryPresenter(
|
||||
@Assisted private val pushHistoryNavigator: PushHistoryNavigator,
|
||||
private val pushService: PushService,
|
||||
matrixClient: MatrixClient,
|
||||
) : Presenter<PushHistoryState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(pushHistoryNavigator: PushHistoryNavigator): PushHistoryPresenter
|
||||
}
|
||||
|
||||
private val sessionId = matrixClient.sessionId
|
||||
|
||||
@Composable
|
||||
override fun present(): PushHistoryState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -41,6 +59,7 @@ class PushHistoryPresenter(
|
|||
}
|
||||
}.collectAsState(emptyList())
|
||||
var resetAction: AsyncAction<Unit> by remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
var showNotSameAccountError by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleEvents(event: PushHistoryEvents) {
|
||||
when (event) {
|
||||
|
|
@ -60,6 +79,14 @@ class PushHistoryPresenter(
|
|||
}
|
||||
PushHistoryEvents.ClearDialog -> {
|
||||
resetAction = AsyncAction.Uninitialized
|
||||
showNotSameAccountError = false
|
||||
}
|
||||
is PushHistoryEvents.NavigateTo -> {
|
||||
if (event.sessionId != sessionId) {
|
||||
showNotSameAccountError = true
|
||||
} else {
|
||||
pushHistoryNavigator.navigateTo(event.roomId, event.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +96,7 @@ class PushHistoryPresenter(
|
|||
pushHistoryItems = pushHistory.toImmutableList(),
|
||||
showOnlyErrors = showOnlyErrors,
|
||||
resetAction = resetAction,
|
||||
showNotSameAccountError = showNotSameAccountError,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@ data class PushHistoryState(
|
|||
val pushHistoryItems: ImmutableList<PushHistoryItem>,
|
||||
val showOnlyErrors: Boolean,
|
||||
val resetAction: AsyncAction<Unit>,
|
||||
val showNotSameAccountError: Boolean,
|
||||
val eventSink: (PushHistoryEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ open class PushHistoryStateProvider : PreviewParameterProvider<PushHistoryState>
|
|||
aPushHistoryState(
|
||||
resetAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
aPushHistoryState(
|
||||
showNotSameAccountError = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -48,12 +51,14 @@ fun aPushHistoryState(
|
|||
pushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||
showOnlyErrors: Boolean = false,
|
||||
resetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
showNotSameAccountError: Boolean = false,
|
||||
eventSink: (PushHistoryEvents) -> Unit = {},
|
||||
) = PushHistoryState(
|
||||
pushCounter = pushCounter,
|
||||
pushHistoryItems = pushHistoryItems.toImmutableList(),
|
||||
showOnlyErrors = showOnlyErrors,
|
||||
resetAction = resetAction,
|
||||
showNotSameAccountError = showNotSameAccountError,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -48,9 +49,6 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
|
|||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.troubleshoot.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -60,7 +58,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
fun PushHistoryView(
|
||||
state: PushHistoryState,
|
||||
onBackClick: () -> Unit,
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
|
@ -123,7 +120,6 @@ fun PushHistoryView(
|
|||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
state = state,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -142,12 +138,18 @@ fun PushHistoryView(
|
|||
},
|
||||
onErrorDismiss = {},
|
||||
)
|
||||
|
||||
if (state.showNotSameAccountError) {
|
||||
ErrorDialog(
|
||||
content = "Please switch account first to navigate to the event.",
|
||||
onSubmit = { state.eventSink(PushHistoryEvents.ClearDialog) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PushHistoryContent(
|
||||
state: PushHistoryState,
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -173,7 +175,7 @@ private fun PushHistoryContent(
|
|||
val roomId = pushHistory.roomId
|
||||
val eventId = pushHistory.eventId
|
||||
if (sessionId != null && roomId != null && eventId != null) {
|
||||
onItemClick(sessionId, roomId, eventId)
|
||||
state.eventSink(PushHistoryEvents.NavigateTo(sessionId, roomId, eventId))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -271,6 +273,5 @@ internal fun PushHistoryViewPreview(
|
|||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onItemClick = { _, _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_screen_action">"Изпълняване на тестове"</string>
|
||||
<string name="troubleshoot_notifications_screen_action_again">"Изпълняване на тестовете отново"</string>
|
||||
<string name="troubleshoot_notifications_screen_success">"Всички тестове преминаха успешно."</string>
|
||||
<string name="troubleshoot_notifications_screen_title">"Отстраняване на неизправности с известията"</string>
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_push_history_title">"Leküldési értesítés előzmények"</string>
|
||||
<string name="screen_push_history_title">"Leküldéses értesítések előzmények"</string>
|
||||
<string name="troubleshoot_notifications_screen_action">"Tesztek futtatása"</string>
|
||||
<string name="troubleshoot_notifications_screen_action_again">"Tesztek újbóli futtatása"</string>
|
||||
<string name="troubleshoot_notifications_screen_failure">"Egyes tesztek sikertelenek voltak. Ellenőrizze a részleteket."</string>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
||||
|
|
@ -32,15 +32,21 @@ class DefaultPushHistoryEntryPointTest {
|
|||
PushHistoryNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = PushHistoryPresenter(
|
||||
pushService = FakePushService(),
|
||||
),
|
||||
presenterFactory = {
|
||||
PushHistoryPresenter(
|
||||
pushHistoryNavigator = object : PushHistoryNavigator {
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError()
|
||||
},
|
||||
pushService = FakePushService(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
)
|
||||
},
|
||||
screenTracker = FakeScreenTracker(),
|
||||
)
|
||||
}
|
||||
val callback = object : PushHistoryEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) = lambdaError()
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,19 @@ package io.element.android.libraries.troubleshoot.impl.history
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -29,6 +39,7 @@ class PushHistoryPresenterTest {
|
|||
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||
assertThat(initialState.showOnlyErrors).isFalse()
|
||||
assertThat(initialState.resetAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(initialState.showNotSameAccountError).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,11 +130,57 @@ class PushHistoryPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - item click current account`() = runTest {
|
||||
val pushHistoryNavigatorResult = lambdaRecorder<RoomId, EventId, Unit> { _, _ -> }
|
||||
val presenter = createPushHistoryPresenter(
|
||||
pushHistoryNavigator = { roomId, eventId ->
|
||||
pushHistoryNavigatorResult(roomId, eventId)
|
||||
}
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(
|
||||
PushHistoryEvents.NavigateTo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
pushHistoryNavigatorResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - item click not current account`() = runTest {
|
||||
val presenter = createPushHistoryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(
|
||||
PushHistoryEvents.NavigateTo(
|
||||
sessionId = A_SESSION_ID_2,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
assertThat(awaitItem().showNotSameAccountError).isTrue()
|
||||
// Reset error
|
||||
initialState.eventSink(PushHistoryEvents.ClearDialog)
|
||||
assertThat(awaitItem().showNotSameAccountError).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPushHistoryPresenter(
|
||||
pushHistoryNavigator: PushHistoryNavigator = PushHistoryNavigator { _, _ -> lambdaError() },
|
||||
pushService: PushService = FakePushService(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
): PushHistoryPresenter {
|
||||
return PushHistoryPresenter(
|
||||
pushHistoryNavigator = pushHistoryNavigator,
|
||||
pushService = pushService,
|
||||
matrixClient = matrixClient,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,20 +14,14 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
|||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_FORMATTED_DATE
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithThreeParams
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
|
|
@ -103,9 +97,8 @@ class PushHistoryViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a valid event invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||
val onItemClick = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
fun `clicking on a valid event emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>()
|
||||
rule.setPushHistoryView(
|
||||
aPushHistoryState(
|
||||
pushHistoryItems = listOf(
|
||||
|
|
@ -118,25 +111,26 @@ class PushHistoryViewTest {
|
|||
),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
rule.onNodeWithText(A_FORMATTED_DATE).performClick()
|
||||
onItemClick.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
eventsRecorder.assertSingle(
|
||||
PushHistoryEvents.NavigateTo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPushHistoryView(
|
||||
state: PushHistoryState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit = EnsureNeverCalledWithThreeParams(),
|
||||
) {
|
||||
setContent {
|
||||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_add_reaction">"Добавяне на реакция: %1$s"</string>
|
||||
<string name="a11y_delete">"Изтриване"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"%1$d въведена цифра"</item>
|
||||
<item quantity="other">"%1$d въведени цифри"</item>
|
||||
</plurals>
|
||||
<string name="a11y_edit_room_address_hint">"Пълният адрес ще бъде %1$s"</string>
|
||||
<string name="a11y_encryption_details">"Подробности за шифроването"</string>
|
||||
<string name="a11y_hide_password">"Скриване на паролата"</string>
|
||||
<string name="a11y_join_call">"Присъединяване към обаждане"</string>
|
||||
<string name="a11y_jump_to_bottom">"Скок към най-долу"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Само споменавания"</string>
|
||||
<string name="a11y_notifications_muted">"Заглушено"</string>
|
||||
<string name="a11y_notifications_new_mentions">"Нови споменавания"</string>
|
||||
<string name="a11y_notifications_new_messages">"Нови съобщения"</string>
|
||||
<string name="a11y_notifications_ongoing_call">"Текущо обаждане"</string>
|
||||
<string name="a11y_page_n">"Страница %1$d"</string>
|
||||
<string name="a11y_pause">"Пауза"</string>
|
||||
<string name="a11y_pin_field">"PIN поле"</string>
|
||||
|
|
@ -26,16 +33,20 @@
|
|||
<string name="a11y_remove_reaction">"Премахване на реакция с %1$s"</string>
|
||||
<string name="a11y_send_files">"Изпращане на файлове"</string>
|
||||
<string name="a11y_show_password">"Показване на паролата"</string>
|
||||
<string name="a11y_start_call">"Започнете обаждане"</string>
|
||||
<string name="a11y_user_menu">"Потребителско меню"</string>
|
||||
<string name="a11y_view_details">"Вижте подробности"</string>
|
||||
<string name="action_accept">"Приемане"</string>
|
||||
<string name="action_add_to_timeline">"Добавяне към хронологията"</string>
|
||||
<string name="action_back">"Назад"</string>
|
||||
<string name="action_call">"Обаждане"</string>
|
||||
<string name="action_cancel">"Отказ"</string>
|
||||
<string name="action_choose_photo">"Избор на снимка"</string>
|
||||
<string name="action_clear">"Изчистване"</string>
|
||||
<string name="action_close">"Затваряне"</string>
|
||||
<string name="action_complete_verification">"Завършване на потвърждаването"</string>
|
||||
<string name="action_confirm">"Потвърждаване"</string>
|
||||
<string name="action_confirm_password">"Потвърдете паролата"</string>
|
||||
<string name="action_continue">"Продължаване"</string>
|
||||
<string name="action_copy">"Копиране"</string>
|
||||
<string name="action_copy_link">"Копиране на връзката"</string>
|
||||
|
|
@ -68,6 +79,7 @@
|
|||
<string name="action_leave">"Напускане"</string>
|
||||
<string name="action_leave_conversation">"Напускане на разговора"</string>
|
||||
<string name="action_leave_room">"Напускане на стаята"</string>
|
||||
<string name="action_leave_space">"Напускане на пространството"</string>
|
||||
<string name="action_load_more">"Зареждане на още"</string>
|
||||
<string name="action_manage_account">"Управление на профила"</string>
|
||||
<string name="action_manage_devices">"Управление на устройствата"</string>
|
||||
|
|
@ -85,7 +97,11 @@
|
|||
<string name="action_remove_message">"Премахване на съобщението"</string>
|
||||
<string name="action_reply">"Отговор"</string>
|
||||
<string name="action_reply_in_thread">"Отговор в нишка"</string>
|
||||
<string name="action_report">"Докладване"</string>
|
||||
<string name="action_report_bug">"Докладване на грешка"</string>
|
||||
<string name="action_report_content">"Докладване на съдържанието"</string>
|
||||
<string name="action_report_room">"Докладване на стаята"</string>
|
||||
<string name="action_reset">"Нулиране"</string>
|
||||
<string name="action_retry">"Повторен опит"</string>
|
||||
<string name="action_retry_decryption">"Повторен опит за разшифроване"</string>
|
||||
<string name="action_save">"Запазване"</string>
|
||||
|
|
@ -102,25 +118,35 @@
|
|||
<string name="action_start">"Започване"</string>
|
||||
<string name="action_start_chat">"Започване на чат"</string>
|
||||
<string name="action_start_verification">"Започване на потвърждаването"</string>
|
||||
<string name="action_static_map_load">"Докоснете за зареждане на карта"</string>
|
||||
<string name="action_take_photo">"Снимка"</string>
|
||||
<string name="action_tap_for_options">"Докоснете за опции"</string>
|
||||
<string name="action_try_again">"Повторен опит"</string>
|
||||
<string name="action_unpin">"Откачване"</string>
|
||||
<string name="action_view_source">"Преглед на източника"</string>
|
||||
<string name="action_yes">"Да"</string>
|
||||
<string name="action_yes_try_again">"Да, опитай отново"</string>
|
||||
<string name="common_about">"Относно"</string>
|
||||
<string name="common_acceptable_use_policy">"Политика за приемлива употреба"</string>
|
||||
<string name="common_add_account">"Добавяне на акаунт"</string>
|
||||
<string name="common_advanced_settings">"Разширени настройки"</string>
|
||||
<string name="common_analytics">"Статистика"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Напуснахте стаята"</string>
|
||||
<string name="common_appearance">"Облик"</string>
|
||||
<string name="common_audio">"Аудио"</string>
|
||||
<string name="common_blocked_users">"Блокирани потребители"</string>
|
||||
<string name="common_bubbles">"Мехурчета"</string>
|
||||
<string name="common_call_started">"Започнато обаждане"</string>
|
||||
<string name="common_chat_backup">"Резервно копие на чатовете"</string>
|
||||
<string name="common_copyright">"Авторски права"</string>
|
||||
<string name="common_creating_room">"Създаване на стая…"</string>
|
||||
<string name="common_dark">"Тъмен"</string>
|
||||
<string name="common_decryption_error">"Грешка при разшифроване"</string>
|
||||
<string name="common_description">"Описание"</string>
|
||||
<string name="common_developer_options">"Опции за разработчици"</string>
|
||||
<string name="common_direct_chat">"Директен чат"</string>
|
||||
<string name="common_do_not_show_this_again">"Не показвай това отново"</string>
|
||||
<string name="common_download_failed">"Неуспешно изтегляне"</string>
|
||||
<string name="common_downloading">"Изтегля се"</string>
|
||||
<string name="common_edited_suffix">"(редактирано)"</string>
|
||||
<string name="common_editing">"Редактиране"</string>
|
||||
|
|
@ -132,18 +158,22 @@
|
|||
<string name="common_error">"Грешка"</string>
|
||||
<string name="common_everyone">"Всеки"</string>
|
||||
<string name="common_favourite">"Фаворизиране"</string>
|
||||
<string name="common_favourited">"Фаворизирано"</string>
|
||||
<string name="common_file">"Файл"</string>
|
||||
<string name="common_file_deleted">"Файлът е изтрит"</string>
|
||||
<string name="common_file_saved">"Файлът е запазен"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Файлът е запазен в Изтеглени"</string>
|
||||
<string name="common_forward_message">"Препращане на съобщението"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Изображение"</string>
|
||||
<string name="common_in_reply_to">"В отговор на %1$s"</string>
|
||||
<string name="common_install_apk_android">"Инсталиране на APK"</string>
|
||||
<string name="common_invite_unknown_profile">"Този Matrix ID не може да бъде намерен, така че поканата може да не бъде получена."</string>
|
||||
<string name="common_leaving_room">"Стаята се напуска"</string>
|
||||
<string name="common_light">"Светъл"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Връзката е копирана в клипборда"</string>
|
||||
<string name="common_loading">"Зарежда се…"</string>
|
||||
<string name="common_loading_more">"Зарежда се още…"</string>
|
||||
<plurals name="common_many_members">
|
||||
<item quantity="one">"%d друг"</item>
|
||||
<item quantity="other">"%d други"</item>
|
||||
|
|
@ -158,13 +188,18 @@
|
|||
<string name="common_modern">"Модерно"</string>
|
||||
<string name="common_mute">"Заглушаване"</string>
|
||||
<string name="common_no_results">"Няма резултати"</string>
|
||||
<string name="common_no_room_name">"Няма име на стая"</string>
|
||||
<string name="common_no_space_name">"Няма име на пространство"</string>
|
||||
<string name="common_not_encrypted">"Без шифроване"</string>
|
||||
<string name="common_offline">"Офлайн"</string>
|
||||
<string name="common_open_source_licenses">"Лицензи за отворен код"</string>
|
||||
<string name="common_or">"или"</string>
|
||||
<string name="common_password">"Парола"</string>
|
||||
<string name="common_people">"Хора"</string>
|
||||
<string name="common_permalink">"Постоянна връзка"</string>
|
||||
<string name="common_permission">"Разрешение"</string>
|
||||
<string name="common_pinned">"Закачено"</string>
|
||||
<string name="common_please_check_internet_connection">"Моля, проверете вашата интернет връзка"</string>
|
||||
<string name="common_please_wait">"Моля, изчакайте…"</string>
|
||||
<string name="common_poll_end_confirmation">"Сигурни ли сте, че искате да приключите тази анкета?"</string>
|
||||
<string name="common_poll_summary">"Анкета: %1$s"</string>
|
||||
|
|
@ -174,19 +209,32 @@
|
|||
<item quantity="one">"%d глас"</item>
|
||||
<item quantity="other">"%d гласа"</item>
|
||||
</plurals>
|
||||
<string name="common_preparing">"Подготвя се…"</string>
|
||||
<string name="common_privacy_policy">"Политика за поверителност"</string>
|
||||
<string name="common_private_room">"Частна стая"</string>
|
||||
<string name="common_public_room">"Общодостъпна стая"</string>
|
||||
<string name="common_public_space">"Общодостъпно пространство"</string>
|
||||
<string name="common_reaction">"Реакция"</string>
|
||||
<string name="common_reactions">"Реакции"</string>
|
||||
<string name="common_reason">"Причина"</string>
|
||||
<string name="common_recovery_key">"Ключ за възстановяване"</string>
|
||||
<string name="common_refreshing">"Опреснява се…"</string>
|
||||
<plurals name="common_replies">
|
||||
<item quantity="one">"%1$d отговор"</item>
|
||||
<item quantity="other">"%1$d отговора"</item>
|
||||
</plurals>
|
||||
<string name="common_replying_to">"В отговор на %1$s"</string>
|
||||
<string name="common_report_a_bug">"Съобщаване за грешка"</string>
|
||||
<string name="common_report_a_problem">"Съобщаване за проблем"</string>
|
||||
<string name="common_report_submitted">"Докладът е изпратен"</string>
|
||||
<string name="common_rich_text_editor">"Редактор на богат текст"</string>
|
||||
<string name="common_room">"Стая"</string>
|
||||
<string name="common_room_name">"Име на стаята"</string>
|
||||
<string name="common_room_name_placeholder">"напр. името на вашия проект"</string>
|
||||
<plurals name="common_rooms">
|
||||
<item quantity="one">"%1$d стая"</item>
|
||||
<item quantity="other">"%1$d стаи"</item>
|
||||
</plurals>
|
||||
<string name="common_saving">"Запазва се"</string>
|
||||
<string name="common_screen_lock">"Заключване на екрана"</string>
|
||||
<string name="common_search_for_someone">"Търсене на някого"</string>
|
||||
|
|
@ -197,20 +245,28 @@
|
|||
<string name="common_sending_failed">"Изпращането е неуспешно"</string>
|
||||
<string name="common_sent">"Изпратено"</string>
|
||||
<string name="common_server_not_supported">"Сървърът не се поддържа"</string>
|
||||
<string name="common_server_url">"URL адрес на сървъра"</string>
|
||||
<string name="common_settings">"Настройки"</string>
|
||||
<string name="common_shared_location">"Споделено местоположение"</string>
|
||||
<string name="common_signing_out">"Излизате"</string>
|
||||
<string name="common_something_went_wrong_message">"Възникна проблем. Моля, опитайте отново."</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="one">"%1$d пространство"</item>
|
||||
<item quantity="other">"%1$d пространства"</item>
|
||||
</plurals>
|
||||
<string name="common_starting_chat">"Започване на чат…"</string>
|
||||
<string name="common_sticker">"Стикер"</string>
|
||||
<string name="common_success">"Успешно"</string>
|
||||
<string name="common_suggestions">"Предложения"</string>
|
||||
<string name="common_syncing">"Синхронизиране"</string>
|
||||
<string name="common_system">"Система"</string>
|
||||
<string name="common_text">"Текст"</string>
|
||||
<string name="common_third_party_notices">"Уведомления от трети страни"</string>
|
||||
<string name="common_thread">"Нишка"</string>
|
||||
<string name="common_topic">"Тема"</string>
|
||||
<string name="common_topic_placeholder">"За какво се отнася тази стая?"</string>
|
||||
<string name="common_unable_to_decrypt">"Не може да се разшифрова"</string>
|
||||
<string name="common_unable_to_invite_message">"Поканите не можаха да бъдат изпратени до един или повече потребители."</string>
|
||||
<string name="common_unable_to_invite_title">"Не може да се изпрати покана(и)"</string>
|
||||
<string name="common_unlock">"Отключване"</string>
|
||||
<string name="common_unmute">"Раззаглушаване"</string>
|
||||
|
|
@ -225,16 +281,35 @@
|
|||
<string name="common_verify_user">"Потвърждаване на потребителя"</string>
|
||||
<string name="common_video">"Видео"</string>
|
||||
<string name="common_voice_message">"Гласово съобщение"</string>
|
||||
<string name="common_waiting">"Изчаква се…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"В очакване на това съобщение"</string>
|
||||
<string name="common_you">"Вие"</string>
|
||||
<string name="dialog_title_confirmation">"Потвърждение"</string>
|
||||
<string name="dialog_title_error">"Грешка"</string>
|
||||
<string name="dialog_title_success">"Успешно"</string>
|
||||
<string name="dialog_title_warning">"Внимание"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Неуспешно създаване на постоянна връзка"</string>
|
||||
<string name="error_failed_loading_map">"%1$s не успя да зареди картата. Моля, опитайте отново по-късно."</string>
|
||||
<string name="error_failed_loading_messages">"Неуспешно зареждане на съобщения"</string>
|
||||
<string name="error_failed_locating_user">"%1$s няма достъп до вашето местоположение. Моля, опитайте отново по-късно."</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s няма разрешение за достъп до вашето местоположение. Можете да активирате достъпа в Настройки."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s няма разрешение за достъп до вашето местоположение. Активирайте достъпа по-долу."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Някои съобщения не са изпратени"</string>
|
||||
<string name="error_unknown">"Съжаляваме, възникна грешка"</string>
|
||||
<string name="event_shield_reason_sent_in_clear">"Без шифроване"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Присъединете се към мен в %1$s"</string>
|
||||
<string name="invite_friends_text">"Хей, говорете с мен в %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Неуспешен избор на мултимедия, моля, опитайте отново."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Натиснете върху съобщение и изберете „%1$s“, за да го включите тук."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Закачете важни съобщения, за да могат лесно да бъдат намерени"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
<item quantity="one">"%1$d закачено съобщение"</item>
|
||||
<item quantity="other">"%1$d закачени съобщения"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Закачени съобщения"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Неуспешна обработка на мултимедия за качване, моля, опитайте отново."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не могат да бъдат извлечени потребителските данни"</string>
|
||||
<string name="screen_share_location_title">"Споделяне на местоположение"</string>
|
||||
<string name="screen_share_my_location_action">"Споделяне на моето местоположение"</string>
|
||||
<string name="screen_share_open_apple_maps">"Отваряне в Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@
|
|||
<string name="common_dark">"Tmavé"</string>
|
||||
<string name="common_decryption_error">"Chyba dešifrování"</string>
|
||||
<string name="common_description">"Popis"</string>
|
||||
<string name="common_deselect_all">"Odznačit vše"</string>
|
||||
<string name="common_developer_options">"Možnosti pro vývojáře"</string>
|
||||
<string name="common_device_id">"ID zařízení"</string>
|
||||
<string name="common_direct_chat">"Přímý chat"</string>
|
||||
|
|
@ -250,6 +251,7 @@ Důvod: %1$s."</string>
|
|||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||
<string name="common_no_results">"Žádné výsledky"</string>
|
||||
<string name="common_no_room_name">"Žádný název místnosti"</string>
|
||||
<string name="common_no_space_name">"Žádný název prostoru"</string>
|
||||
<string name="common_not_encrypted">"Nešifrováno"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_open_source_licenses">"Licence s otevřeným zdrojovým kódem"</string>
|
||||
|
|
@ -305,6 +307,7 @@ Důvod: %1$s."</string>
|
|||
<string name="common_security">"Zabezpečení"</string>
|
||||
<string name="common_seen_by">"Viděno"</string>
|
||||
<string name="common_select_account">"Vybrat účet"</string>
|
||||
<string name="common_select_all">"Vybrat vše"</string>
|
||||
<string name="common_send_to">"Odeslat do"</string>
|
||||
<string name="common_sending">"Odesílání…"</string>
|
||||
<string name="common_sending_failed">"Odeslání se nezdařilo"</string>
|
||||
|
|
@ -419,9 +422,6 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
|
||||
<string name="screen_bottom_sheet_leave_space_subtitle">"Tím budete také odstraněni ze všech místností v tomto prostoru."</string>
|
||||
<string name="screen_bottom_sheet_leave_space_subtitle_admin">"Tímto budete také odstraněni ze všech místností v tomto prostoru, včetně těch, jejichž jediným správcem jste:"</string>
|
||||
<string name="screen_bottom_sheet_leave_space_title">"Opustit %1$s?"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Snímek obrazovky"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Možnosti"</string>
|
||||
|
|
@ -466,6 +466,7 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
|
||||
<string name="screen_space_list_description">"Prostory, které jste vytvořili nebo se k nim připojili."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
|
||||
<string name="screen_space_list_title">"Prostory"</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>
|
||||
|
|
|
|||
|
|
@ -445,9 +445,6 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
|
|||
<string name="invite_friends_text">"Hei, siaradwch â mi ar %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"Android %1$s"</string>
|
||||
<string name="preference_rageshake">"Rageshake i adrodd gwall"</string>
|
||||
<string name="screen_bottom_sheet_leave_space_subtitle">"Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn."</string>
|
||||
<string name="screen_bottom_sheet_leave_space_subtitle_admin">"Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn, gan gynnwys y rhai rydych chi\'n unig weinyddwr ar eu cyfer:"</string>
|
||||
<string name="screen_bottom_sheet_leave_space_title">"Gadael %1$s ?"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Llun sgrin"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Dewisiadau"</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