Create or retrieve DM

This commit is contained in:
Florian Renaud 2023-03-24 16:01:14 +01:00
parent 7aa1c26e2e
commit 6450fc5724
17 changed files with 220 additions and 28 deletions

View file

@ -32,6 +32,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
@ -178,7 +179,16 @@ class LoggedInFlowNode @AssistedInject constructor(
.build() .build()
} }
NavTarget.CreateRoom -> { NavTarget.CreateRoom -> {
createRoomEntryPoint.createNode(this, buildContext) val callback = object : CreateRoomEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomId))
}
}
createRoomEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
} }
NavTarget.VerifySession -> { NavTarget.VerifySession -> {
verifySessionEntryPoint.createNode(this, buildContext) verifySessionEntryPoint.createNode(this, buildContext)

View file

@ -24,4 +24,5 @@ android {
dependencies { dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
} }

View file

@ -16,6 +16,21 @@
package io.element.android.features.createroom.api package io.element.android.features.createroom.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint 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.RoomId
interface CreateRoomEntryPoint : SimpleFeatureEntryPoint interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId)
}
}

View file

@ -23,17 +23,20 @@ import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -64,6 +67,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
override fun onCreateNewRoom() { override fun onCreateNewRoom() {
backstack.push(NavTarget.NewRoom) backstack.push(NavTarget.NewRoom)
} }
override fun onOpenRoom(roomId: RoomId) {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
}
} }
createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback)) createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback))
} }

View file

@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -26,7 +27,21 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint { class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<CreateRoomFlowNode>(buildContext) override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : CreateRoomEntryPoint.NodeBuilder {
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<CreateRoomFlowNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -19,6 +19,8 @@ package io.element.android.features.createroom.impl.root
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
sealed interface CreateRoomRootEvents { sealed interface CreateRoomRootEvents {
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
object InvitePeople : CreateRoomRootEvents object InvitePeople : CreateRoomRootEvents
data class SelectUser(val matrixUser: MatrixUser) : CreateRoomRootEvents
data class CreateDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
object CancelCreateDM : CreateRoomRootEvents
} }

View file

@ -16,7 +16,6 @@
package io.element.android.features.createroom.impl.root package io.element.android.features.createroom.impl.root
import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -27,7 +26,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class CreateRoomRootNode @AssistedInject constructor( class CreateRoomRootNode @AssistedInject constructor(
@ -38,15 +37,17 @@ class CreateRoomRootNode @AssistedInject constructor(
interface Callback : Plugin { interface Callback : Plugin {
fun onCreateNewRoom() fun onCreateNewRoom()
fun onOpenRoom(roomId: RoomId)
} }
private fun onCreateNewRoom() { private val callback = object : Callback {
plugins<Callback>().forEach { it.onCreateNewRoom() } override fun onCreateNewRoom() {
} plugins<Callback>().forEach { it.onCreateNewRoom() }
}
sealed interface NavTarget : Parcelable { override fun onOpenRoom(roomId: RoomId) {
@Parcelize plugins<Callback>().forEach { it.onOpenRoom(roomId) }
object Root : NavTarget }
} }
@Composable @Composable
@ -56,7 +57,8 @@ class CreateRoomRootNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onClosePressed = this::navigateUp, onClosePressed = this::navigateUp,
onNewRoomClicked = this::onCreateNewRoom, onNewRoomClicked = callback::onCreateNewRoom,
onOpenDM = callback::onOpenRoom,
) )
} }
} }

View file

@ -17,16 +17,30 @@
package io.element.android.features.createroom.impl.root package io.element.android.features.createroom.impl.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.selectusers.api.SelectUsersEvents
import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenter
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
import io.element.android.features.selectusers.api.SelectionMode import io.element.android.features.selectusers.api.SelectionMode
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import timber.log.Timber import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class CreateRoomRootPresenter @Inject constructor( class CreateRoomRootPresenter @Inject constructor(
private val presenterFactory: SelectUsersPresenter.Factory, private val presenterFactory: SelectUsersPresenter.Factory,
private val matrixClient: MatrixClient,
) : Presenter<CreateRoomRootState> { ) : Presenter<CreateRoomRootState> {
private val presenter by lazy { private val presenter by lazy {
@ -37,20 +51,44 @@ class CreateRoomRootPresenter @Inject constructor(
override fun present(): CreateRoomRootState { override fun present(): CreateRoomRootState {
val selectUsersState = presenter.present() val selectUsersState = presenter.present()
val localCoroutineScope = rememberCoroutineScope()
var showCreateDmConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
fun handleEvents(event: CreateRoomRootEvents) { fun handleEvents(event: CreateRoomRootEvents) {
when (event) { when (event) {
is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser) is CreateRoomRootEvents.SelectUser -> {
val existingDM = matrixClient.findDM(event.matrixUser.id)
if (existingDM == null) {
showCreateDmConfirmationDialog = true
} else {
startDmAction.value = Async.Success(existingDM.roomId)
}
}
is CreateRoomRootEvents.CreateDM -> {
showCreateDmConfirmationDialog = false
localCoroutineScope.createDM(event.matrixUser, startDmAction)
}
CreateRoomRootEvents.CancelCreateDM -> {
showCreateDmConfirmationDialog = false
selectUsersState.eventSink(SelectUsersEvents.ClearSelection)
}
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
} }
} }
return CreateRoomRootState( return CreateRoomRootState(
selectUsersState = selectUsersState, selectUsersState = selectUsersState,
showCreateDmConfirmationDialog = showCreateDmConfirmationDialog,
startDmAction = startDmAction.value,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun handleStartDM(matrixUser: MatrixUser) { private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action suspend {
matrixClient.createDM(user.id).getOrThrow()
}.execute(startDmAction)
} }
} }

View file

@ -17,8 +17,12 @@
package io.element.android.features.createroom.impl.root package io.element.android.features.createroom.impl.root
import io.element.android.features.selectusers.api.SelectUsersState import io.element.android.features.selectusers.api.SelectUsersState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
data class CreateRoomRootState( data class CreateRoomRootState(
val selectUsersState: SelectUsersState, val selectUsersState: SelectUsersState,
val showCreateDmConfirmationDialog: Boolean,
val startDmAction: Async<RoomId>,
val eventSink: (CreateRoomRootEvents) -> Unit, val eventSink: (CreateRoomRootEvents) -> Unit,
) )

View file

@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.selectusers.api.aSelectUsersState import io.element.android.features.selectusers.api.aSelectUsersState
import io.element.android.libraries.architecture.Async
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> { open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
override val values: Sequence<CreateRoomRootState> override val values: Sequence<CreateRoomRootState>
@ -28,5 +29,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
fun aCreateRoomRootState() = CreateRoomRootState( fun aCreateRoomRootState() = CreateRoomRootState(
eventSink = {}, eventSink = {},
selectUsersState = aSelectUsersState(), startDmAction = Async.Uninitialized,
showCreateDmConfirmationDialog = false,
selectUsersState = aSelectUsersState(),
) )

View file

@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -39,6 +40,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.selectusers.api.SelectUsersView import io.element.android.features.selectusers.api.SelectUsersView
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
@ -46,6 +50,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold 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.Text
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.designsystem.R as DrawableR import io.element.android.libraries.designsystem.R as DrawableR
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@ -56,7 +62,14 @@ fun CreateRoomRootView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClosePressed: () -> Unit = {}, onClosePressed: () -> Unit = {},
onNewRoomClicked: () -> Unit = {}, onNewRoomClicked: () -> Unit = {},
onOpenDM: (RoomId) -> Unit = {},
) { ) {
if (state.startDmAction is Async.Success) {
LaunchedEffect(state.startDmAction) {
onOpenDM(state.startDmAction.state)
}
}
Scaffold( Scaffold(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
topBar = { topBar = {
@ -72,7 +85,7 @@ fun CreateRoomRootView(
SelectUsersView( SelectUsersView(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
state = state.selectUsersState, state = state.selectUsersState,
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) }, onUserSelected = { state.eventSink(CreateRoomRootEvents.SelectUser(it)) },
) )
if (!state.selectUsersState.isSearchActive) { if (!state.selectUsersState.isSearchActive) {
@ -83,6 +96,12 @@ fun CreateRoomRootView(
} }
} }
} }
CreateDmConfirmationDialog(state)
if (state.startDmAction is Async.Loading) {
ProgressDialog(text = "Creating room...")
}
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -153,6 +172,26 @@ fun CreateRoomActionButton(
} }
} }
@Composable
fun CreateDmConfirmationDialog(
state: CreateRoomRootState,
modifier: Modifier = Modifier,
) {
if (state.showCreateDmConfirmationDialog) {
val selectedUser = state.selectUsersState.selectedUsers.firstOrNull()
if (selectedUser != null) {
ConfirmationDialog(
modifier = modifier,
title = "Start chat",
content = "You're about starting a chat with ${selectedUser.getBestName()}, do you want to continue?",
submitText = stringResource(io.element.android.libraries.ui.strings.R.string._continue),
onSubmitClicked = { state.eventSink(CreateRoomRootEvents.CreateDM(selectedUser)) },
onDismiss = { state.eventSink(CreateRoomRootEvents.CancelCreateDM) },
)
}
}
}
@Preview @Preview
@Composable @Composable
fun CreateRoomRootViewLightPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) = fun CreateRoomRootViewLightPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =

View file

@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -34,13 +35,15 @@ import org.junit.Test
class CreateRoomRootPresenterTests { class CreateRoomRootPresenterTests {
private lateinit var presenter: CreateRoomRootPresenter private lateinit var presenter: CreateRoomRootPresenter
private lateinit var fakeMatrixClient: FakeMatrixClient
@Before @Before
fun setup() { fun setup() {
val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
} }
presenter = CreateRoomRootPresenter(selectUsersPresenter) fakeMatrixClient = FakeMatrixClient()
presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient)
} }
@Test @Test
@ -64,13 +67,13 @@ class CreateRoomRootPresenterTests {
} }
@Test @Test
fun `present - trigger start DM action`() = runTest { fun `present - trigger select user action`() = runTest {
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
val matrixUser = MatrixUser(UserId("@name:matrix.org")) val matrixUser = MatrixUser(UserId("@name:matrix.org"))
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) initialState.eventSink(CreateRoomRootEvents.SelectUser(matrixUser))
} }
} }
} }

View file

@ -22,5 +22,6 @@ sealed interface SelectUsersEvents {
data class UpdateSearchQuery(val query: String) : SelectUsersEvents data class UpdateSearchQuery(val query: String) : SelectUsersEvents
data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents
data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents
object ClearSelection : SelectUsersEvents
data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents
} }

View file

@ -79,6 +79,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor(
localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState)
} }
is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList()
SelectUsersEvents.ClearSelection -> selectedUsers.value = persistentListOf()
} }
} }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
@ -28,6 +29,8 @@ interface MatrixClient : Closeable {
val sessionId: SessionId val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource val roomSummaryDataSource: RoomSummaryDataSource
fun getRoom(roomId: RoomId): MatrixRoom? fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun createDM(userId: UserId): Result<RoomId>
fun findDM(userId: UserId): MatrixRoom?
fun startSync() fun startSync()
fun stopSync() fun stopSync()
fun mediaResolver(): MediaResolver fun mediaResolver(): MediaResolver

View file

@ -37,7 +37,10 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.CreateRoomParameters
import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.RoomPreset
import org.matrix.rustcomponents.sdk.RoomVisibility
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncMode
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
@ -154,6 +157,30 @@ class RustMatrixClient constructor(
) )
} }
override fun findDM(userId: UserId): MatrixRoom? {
val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) }
return roomId?.let { getRoom(it) }
}
override suspend fun createDM(userId: UserId): Result<RoomId> =
withContext(dispatchers.io) {
runCatching {
val roomId = client.createRoom(
CreateRoomParameters(
name = "",
topic = null,
isEncrypted = true,
isDirect = true,
visibility = RoomVisibility.PRIVATE,
preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
invite = listOf(userId.value),
avatar = null,
)
)
RoomId(roomId)
}
}
override fun mediaResolver(): MediaResolver = mediaResolver override fun mediaResolver(): MediaResolver = mediaResolver
override fun sessionVerificationService(): SessionVerificationService = verificationService override fun sessionVerificationService(): SessionVerificationService = verificationService

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
@ -37,12 +38,22 @@ class FakeMatrixClient(
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService() private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService()
) : MatrixClient { ) : MatrixClient {
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
private var logoutFailure: Throwable? = null private var logoutFailure: Throwable? = null
override fun getRoom(roomId: RoomId): MatrixRoom? { override fun getRoom(roomId: RoomId): MatrixRoom? {
return FakeMatrixRoom(roomId) return FakeMatrixRoom(roomId)
} }
override suspend fun createDM(userId: UserId): Result<RoomId> {
return createDmResult
}
override fun findDM(userId: UserId): MatrixRoom? {
return findDmResult
}
override fun startSync() = Unit override fun startSync() = Unit
override fun stopSync() = Unit override fun stopSync() = Unit
@ -51,10 +62,6 @@ class FakeMatrixClient(
return FakeMediaResolver() return FakeMediaResolver()
} }
fun givenLogoutError(failure: Throwable) {
logoutFailure = failure
}
override suspend fun logout() { override suspend fun logout() {
delay(100) delay(100)
logoutFailure?.let { throw it } logoutFailure?.let { throw it }
@ -81,4 +88,18 @@ class FakeMatrixClient(
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
override fun onSlidingSyncUpdate() {} override fun onSlidingSyncUpdate() {}
// Mocks
fun givenLogoutError(failure: Throwable) {
logoutFailure = failure
}
fun givenCreateDmResult(result: Result<RoomId>) {
createDmResult = result
}
fun givenFindDmResult(result: MatrixRoom?) {
findDmResult = result
}
} }