Merge branch 'develop' into feature/bma/push
This commit is contained in:
@@ -32,6 +32,7 @@ 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.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
@@ -199,7 +200,16 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
.build()
|
||||
}
|
||||
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 -> {
|
||||
verifySessionEntryPoint.createNode(this, buildContext)
|
||||
|
||||
@@ -231,6 +231,7 @@ koverMerged {
|
||||
overrideClassFilter {
|
||||
includes += "*Presenter"
|
||||
excludes += "*TemplatePresenter"
|
||||
excludes += "*Fake*Presenter"
|
||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||
}
|
||||
bound {
|
||||
|
||||
1
changelog.d/96.feature
Normal file
1
changelog.d/96.feature
Normal file
@@ -0,0 +1 @@
|
||||
[Create and join rooms] Show or create direct message room
|
||||
@@ -24,4 +24,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,21 @@
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,20 @@ import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
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.root.CreateRoomRootNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -64,6 +67,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||
override fun onCreateNewRoom() {
|
||||
backstack.push(NavTarget.NewRoom)
|
||||
}
|
||||
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
}
|
||||
createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
@@ -26,7 +27,21 @@ import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.root
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
sealed interface CreateRoomRootEvents {
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object InvitePeople : CreateRoomRootEvents
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object CancelStartDM : CreateRoomRootEvents
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -27,7 +26,7 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateRoomRootNode @AssistedInject constructor(
|
||||
@@ -38,15 +37,17 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onCreateNewRoom()
|
||||
fun onOpenRoom(roomId: RoomId)
|
||||
}
|
||||
|
||||
private fun onCreateNewRoom() {
|
||||
plugins<Callback>().forEach { it.onCreateNewRoom() }
|
||||
}
|
||||
private val callback = object : Callback {
|
||||
override fun onCreateNewRoom() {
|
||||
plugins<Callback>().forEach { it.onCreateNewRoom() }
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
override fun onOpenRoom(roomId: RoomId) {
|
||||
plugins<Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -56,7 +57,8 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onClosePressed = this::navigateUp,
|
||||
onNewRoomClicked = this::onCreateNewRoom,
|
||||
onNewRoomClicked = callback::onCreateNewRoom,
|
||||
onOpenDM = callback::onOpenRoom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,29 @@
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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 timber.log.Timber
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor(
|
||||
private val presenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<CreateRoomRootState> {
|
||||
|
||||
private val presenter by lazy {
|
||||
@@ -43,20 +53,37 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||
override fun present(): CreateRoomRootState {
|
||||
val userListState = presenter.present()
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
fun startDm(matrixUser: MatrixUser) {
|
||||
startDmAction.value = Async.Uninitialized
|
||||
val existingDM = matrixClient.findDM(matrixUser.id)
|
||||
if (existingDM == null) {
|
||||
localCoroutineScope.createDM(matrixUser, startDmAction)
|
||||
} else {
|
||||
startDmAction.value = Async.Success(existingDM.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser)
|
||||
is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser)
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized
|
||||
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
|
||||
}
|
||||
}
|
||||
|
||||
return CreateRoomRootState(
|
||||
userListState = userListState,
|
||||
startDmAction = startDmAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleStartDM(matrixUser: MatrixUser) {
|
||||
Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action
|
||||
private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
|
||||
suspend {
|
||||
matrixClient.createDM(user.id).getOrThrow()
|
||||
}.execute(startDmAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class CreateRoomRootState(
|
||||
val userListState: UserListState,
|
||||
val startDmAction: Async<RoomId>,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -18,15 +18,41 @@ package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||
override val values: Sequence<CreateRoomRootState>
|
||||
get() = sequenceOf(
|
||||
aCreateRoomRootState(),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Loading(),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Failure(Throwable()),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.id.value,
|
||||
searchResults = persistentListOf(it),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||
eventSink = {},
|
||||
userListState = aUserListState(),
|
||||
startDmAction = Async.Uninitialized,
|
||||
userListState = aUserListState(),
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -28,17 +29,22 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
@@ -46,6 +52,7 @@ 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.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.designsystem.R as DrawableR
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@@ -56,7 +63,14 @@ fun CreateRoomRootView(
|
||||
modifier: Modifier = Modifier,
|
||||
onClosePressed: () -> Unit = {},
|
||||
onNewRoomClicked: () -> Unit = {},
|
||||
onOpenDM: (RoomId) -> Unit = {},
|
||||
) {
|
||||
if (state.startDmAction is Async.Success) {
|
||||
LaunchedEffect(state.startDmAction) {
|
||||
onOpenDM(state.startDmAction.state)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
topBar = {
|
||||
@@ -69,10 +83,16 @@ fun CreateRoomRootView(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.userListState,
|
||||
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) },
|
||||
onUserSelected = {
|
||||
// Fixme disabled DM creation since it can break the account data which is not correctly synced
|
||||
// uncomment to enable it again or move behind a feature flag
|
||||
Toast.makeText(context, "Create DM feature is disabled.", Toast.LENGTH_SHORT).show()
|
||||
// state.eventSink(CreateRoomRootEvents.StartDM(it))
|
||||
},
|
||||
)
|
||||
|
||||
if (!state.userListState.isSearchActive) {
|
||||
@@ -83,6 +103,25 @@ fun CreateRoomRootView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (state.startDmAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = StringR.string.common_creating_room))
|
||||
}
|
||||
is Async.Failure -> {
|
||||
RetryDialog(
|
||||
content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat),
|
||||
onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||
onRetry = {
|
||||
state.userListState.selectedUsers.firstOrNull()
|
||||
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
||||
// Cancel start DM if there is no more selected user (should not happen)
|
||||
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@@ -22,10 +22,8 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
@@ -37,11 +35,7 @@ class AddPeoplePresenterTests {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val userListFactory = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
val dataSource = FakeMatrixUserDataSource()
|
||||
presenter = AddPeoplePresenter(userListFactory, dataSource)
|
||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeMatrixUserDataSource())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -22,12 +22,18 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
@@ -37,14 +43,15 @@ class CreateRoomRootPresenterTests {
|
||||
|
||||
private lateinit var userListDataSource: FakeMatrixUserDataSource
|
||||
private lateinit var presenter: CreateRoomRootPresenter
|
||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val userListPresenter = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
fakeUserListPresenter = FakeUserListPresenter()
|
||||
fakeMatrixClient = FakeMatrixClient()
|
||||
userListDataSource = FakeMatrixUserDataSource()
|
||||
presenter = CreateRoomRootPresenter(userListPresenter, userListDataSource)
|
||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, fakeMatrixClient)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,13 +75,82 @@ class CreateRoomRootPresenterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger start DM action`() = runTest {
|
||||
fun `present - trigger create DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val createDmResult = Result.success(RoomId("!createDmResult"))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(null)
|
||||
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger retrieve DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult"))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(fakeDmResult)
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger retry create DM action`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
val createDmResult = Result.success(RoomId("!createDmResult"))
|
||||
fakeUserListPresenter.givenState(aUserListState().copy(selectedUsers = persistentListOf(matrixUser)))
|
||||
|
||||
fakeMatrixClient.givenFindDmResult(null)
|
||||
fakeMatrixClient.givenCreateDmError(A_THROWABLE)
|
||||
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
||||
|
||||
// Failure
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterStartDM = awaitItem()
|
||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
// Cancel
|
||||
stateAfterStartDM.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
val stateAfterCancel = awaitItem()
|
||||
assertThat(stateAfterCancel.startDmAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
|
||||
// Failure
|
||||
stateAfterCancel.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterSecondAttempt = awaitItem()
|
||||
assertThat(stateAfterSecondAttempt.startDmAction).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
// Retry with success
|
||||
fakeMatrixClient.givenCreateDmError(null)
|
||||
stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
val stateAfterRetryStartDM = awaitItem()
|
||||
assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -25,6 +23,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.userlist.api)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userlist.test
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
|
||||
class FakeUserListPresenter : UserListPresenter {
|
||||
|
||||
private var state = aUserListState()
|
||||
|
||||
fun givenState(state: UserListState) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): UserListState {
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userlist.test
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
|
||||
class FakeUserListPresenterFactory(
|
||||
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
||||
) : UserListPresenter.Factory {
|
||||
|
||||
override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter = fakeUserListPresenter
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.dialogs
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun RetryDialog(
|
||||
content: String,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = RetryDialogDefaults.title,
|
||||
retryText: String = RetryDialogDefaults.retryText,
|
||||
dismissText: String = RetryDialogDefaults.dismissText,
|
||||
onRetry: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
shape: Shape = AlertDialogDefaults.shape,
|
||||
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
AlertDialog(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Text(content)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onRetry) {
|
||||
Text(retryText)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(dismissText)
|
||||
}
|
||||
},
|
||||
shape = shape,
|
||||
containerColor = containerColor,
|
||||
iconContentColor = iconContentColor,
|
||||
titleContentColor = titleContentColor,
|
||||
textContentColor = textContentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
)
|
||||
}
|
||||
|
||||
object RetryDialogDefaults {
|
||||
val title: String @Composable get() = stringResource(id = StringR.string.dialog_title_error)
|
||||
val retryText: String @Composable get() = stringResource(id = StringR.string.action_retry)
|
||||
val dismissText: String @Composable get() = stringResource(id = StringR.string.action_cancel)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RetryDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RetryDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RetryDialog(
|
||||
content = "Content",
|
||||
)
|
||||
}
|
||||
@@ -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.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.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
@@ -31,6 +32,8 @@ interface MatrixClient : Closeable {
|
||||
val sessionId: SessionId
|
||||
val roomSummaryDataSource: RoomSummaryDataSource
|
||||
fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
suspend fun createDM(userId: UserId): Result<RoomId>
|
||||
fun findDM(userId: UserId): MatrixRoom?
|
||||
fun startSync()
|
||||
fun stopSync()
|
||||
fun mediaResolver(): MediaResolver
|
||||
|
||||
@@ -42,7 +42,10 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
||||
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.SlidingSyncMode
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
||||
@@ -167,6 +170,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 = null,
|
||||
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 sessionVerificationService(): SessionVerificationService = verificationService
|
||||
|
||||
@@ -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.core.RoomId
|
||||
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.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
@@ -44,12 +45,25 @@ class FakeMatrixClient(
|
||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||
) : MatrixClient {
|
||||
|
||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
private var createDmFailure: Throwable? = null
|
||||
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
||||
private var logoutFailure: Throwable? = null
|
||||
|
||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
return FakeMatrixRoom(roomId)
|
||||
}
|
||||
|
||||
override suspend fun createDM(userId: UserId): Result<RoomId> {
|
||||
delay(100)
|
||||
createDmFailure?.let { throw it }
|
||||
return createDmResult
|
||||
}
|
||||
|
||||
override fun findDM(userId: UserId): MatrixRoom? {
|
||||
return findDmResult
|
||||
}
|
||||
|
||||
override fun startSync() = Unit
|
||||
|
||||
override fun stopSync() = Unit
|
||||
@@ -58,10 +72,6 @@ class FakeMatrixClient(
|
||||
return FakeMediaResolver()
|
||||
}
|
||||
|
||||
fun givenLogoutError(failure: Throwable) {
|
||||
logoutFailure = failure
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
delay(100)
|
||||
logoutFailure?.let { throw it }
|
||||
@@ -96,4 +106,22 @@ class FakeMatrixClient(
|
||||
override fun roomMembershipObserver(): RoomMembershipObserver {
|
||||
return RoomMembershipObserver(A_SESSION_ID)
|
||||
}
|
||||
|
||||
// Mocks
|
||||
|
||||
fun givenLogoutError(failure: Throwable?) {
|
||||
logoutFailure = failure
|
||||
}
|
||||
|
||||
fun givenCreateDmResult(result: Result<RoomId>) {
|
||||
createDmResult = result
|
||||
}
|
||||
|
||||
fun givenCreateDmError(failure: Throwable?) {
|
||||
createDmFailure = failure
|
||||
}
|
||||
|
||||
fun givenFindDmResult(result: MatrixRoom?) {
|
||||
findDmResult = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file('matrix-rust-sdk.aar'))
|
||||
2
libraries/rustsdk/build.gradle.kts
Normal file
2
libraries/rustsdk/build.gradle.kts
Normal file
@@ -0,0 +1,2 @@
|
||||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file("matrix-rust-sdk.aar"))
|
||||
@@ -51,7 +51,6 @@ include(":appnav")
|
||||
include(":tests:uitests")
|
||||
include(":anvilannotations")
|
||||
include(":anvilcodegen")
|
||||
include(":libraries:rustsdk")
|
||||
|
||||
include(":samples:minimal")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user