[Room Details] Block & unblock user (#340)
This commit is contained in:
parent
0d327cd543
commit
aef2b38983
35 changed files with 477 additions and 174 deletions
|
|
@ -18,6 +18,7 @@ package io.element.android.appnav
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
|
@ -134,8 +135,18 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
object RoomDetails : NavTarget
|
object RoomDetails : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val timeline = inputs.room.timeline()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
timeline.initialize()
|
||||||
|
onDispose {
|
||||||
|
timeline.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Children(
|
Children(
|
||||||
navModel = backstack,
|
navModel = backstack,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
|
||||||
1
changelog.d/339.feature
Normal file
1
changelog.d/339.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Block & unblock users from room details screen.
|
||||||
|
|
@ -80,13 +80,6 @@ class TimelinePresenter @Inject constructor(
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
timeline.initialize()
|
|
||||||
onDispose {
|
|
||||||
timeline.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return TimelineState(
|
return TimelineState(
|
||||||
highlightedEventId = highlightedEventId.value,
|
highlightedEventId = highlightedEventId.value,
|
||||||
paginationState = paginationState.value,
|
paginationState = paginationState.value,
|
||||||
|
|
|
||||||
|
|
@ -49,23 +49,6 @@ class TimelinePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - makes sure timeline is initialized and disposed`() = runTest {
|
|
||||||
val fakeTimeline = FakeMatrixTimeline()
|
|
||||||
val presenter = TimelinePresenter(
|
|
||||||
timelineItemsFactory = aTimelineItemsFactory(),
|
|
||||||
room = FakeMatrixRoom(matrixTimeline = fakeTimeline),
|
|
||||||
)
|
|
||||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(2)
|
|
||||||
assertThat(fakeTimeline.isInitialized).isTrue()
|
|
||||||
}
|
|
||||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load more`() = runTest {
|
fun `present - load more`() = runTest {
|
||||||
val presenter = TimelinePresenter(
|
val presenter = TimelinePresenter(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.blockuser
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
|
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) {
|
||||||
|
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||||
|
if (state.isBlocked) {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||||
|
icon = Icons.Outlined.Block,
|
||||||
|
onClick = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(R.string.screen_dm_details_block_user),
|
||||||
|
icon = Icons.Outlined.Block,
|
||||||
|
tintColor = LocalColors.current.textActionCritical,
|
||||||
|
onClick = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BlockUserDialogs(state: RoomMemberDetailsState) {
|
||||||
|
when (state.displayConfirmationDialog) {
|
||||||
|
null -> Unit
|
||||||
|
RoomMemberDetailsState.ConfirmationDialog.Block -> {
|
||||||
|
BlockConfirmationDialog(
|
||||||
|
onBlockAction = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) },
|
||||||
|
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RoomMemberDetailsState.ConfirmationDialog.Unblock -> {
|
||||||
|
UnblockConfirmationDialog(
|
||||||
|
onUnblockAction = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) },
|
||||||
|
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
content = stringResource(R.string.screen_dm_details_block_alert_description),
|
||||||
|
submitText = stringResource(R.string.screen_dm_details_block_alert_action),
|
||||||
|
onSubmitClicked = onBlockAction,
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
content = stringResource(R.string.screen_dm_details_unblock_alert_description),
|
||||||
|
submitText = stringResource(R.string.screen_dm_details_unblock_alert_action),
|
||||||
|
onSubmitClicked = onUnblockAction,
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -63,7 +63,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
activityResultLauncher = null,
|
activityResultLauncher = null,
|
||||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
text = permalink,
|
text = permalink,
|
||||||
|
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||||
)
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,12 +89,21 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
||||||
|
fun onShareRoom() {
|
||||||
|
this.onShareRoom(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShareMember(roomMember: RoomMember) {
|
||||||
|
this.onShareMember(context, roomMember)
|
||||||
|
}
|
||||||
|
|
||||||
RoomDetailsView(
|
RoomDetailsView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
goBack = { navigateUp() },
|
goBack = this::navigateUp,
|
||||||
onShareRoom = { onShareRoom(context) },
|
onShareRoom = ::onShareRoom,
|
||||||
onShareMember = { onShareMember(context, it) },
|
onShareMember = ::onShareMember,
|
||||||
openRoomMemberList = ::openRoomMemberList,
|
openRoomMemberList = ::openRoomMemberList,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Async
|
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.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -30,16 +32,32 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
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.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomDetailsPresenter @Inject constructor(
|
class RoomDetailsPresenter @Inject constructor(
|
||||||
private val sessionId: SessionId,
|
private val matrixClient: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
) : Presenter<RoomDetailsState> {
|
) : Presenter<RoomDetailsState> {
|
||||||
|
|
||||||
|
private val roomMemberDetailsPresenter by lazy {
|
||||||
|
val dmMember = runBlocking {
|
||||||
|
room.getDmMember().firstOrNull()
|
||||||
|
}
|
||||||
|
if (dmMember != null) {
|
||||||
|
RoomMemberDetailsPresenter(matrixClient.sessionId, room, dmMember)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomDetailsState {
|
override fun present(): RoomDetailsState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -50,20 +68,16 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
mutableStateOf<RoomDetailsError?>(null)
|
mutableStateOf<RoomDetailsError?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
var memberCount: Async<Int> by remember { mutableStateOf(Async.Loading()) }
|
val memberCount by produceState<Async<Int>>(initialValue = Async.Loading(null)) {
|
||||||
LaunchedEffect(Unit) {
|
room.members().map { it.count() }
|
||||||
withContext(Dispatchers.IO) {
|
.onEach { value = Async.Success(it) }
|
||||||
memberCount = runCatching { room.memberCount() }
|
.catch { value = Async.Failure(it) }
|
||||||
.fold(
|
.launchIn(coroutineScope)
|
||||||
onSuccess = { Async.Success(it) },
|
|
||||||
onFailure = { Async.Failure(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val dmMember = room.getDmMember()
|
val dmMember by room.getDmMember().collectAsState(initial = null)
|
||||||
val roomType = if (dmMember != null) {
|
val roomType = if (dmMember != null) {
|
||||||
RoomDetailsType.Dm(dmMember)
|
RoomDetailsType.Dm(dmMember!!)
|
||||||
} else {
|
} else {
|
||||||
RoomDetailsType.Room
|
RoomDetailsType.Room
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +104,12 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val roomMemberDetailsState = if (dmMember != null) {
|
||||||
|
roomMemberDetailsPresenter?.present()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
return RoomDetailsState(
|
return RoomDetailsState(
|
||||||
roomId = room.roomId.value,
|
roomId = room.roomId.value,
|
||||||
roomName = room.name ?: room.displayName,
|
roomName = room.name ?: room.displayName,
|
||||||
|
|
@ -101,6 +121,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
displayLeaveRoomWarning = leaveRoomWarning,
|
displayLeaveRoomWarning = leaveRoomWarning,
|
||||||
error = error,
|
error = error,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
|
roomMemberDetailsState = roomMemberDetailsState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
data class RoomDetailsState(
|
data class RoomDetailsState(
|
||||||
|
|
@ -33,6 +31,7 @@ data class RoomDetailsState(
|
||||||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||||
val error: RoomDetailsError?,
|
val error: RoomDetailsError?,
|
||||||
val roomType: RoomDetailsType,
|
val roomType: RoomDetailsType,
|
||||||
|
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||||
val eventSink: (RoomDetailsEvent) -> Unit
|
val eventSink: (RoomDetailsEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||||
displayLeaveRoomWarning = null,
|
displayLeaveRoomWarning = null,
|
||||||
error = null,
|
error = null,
|
||||||
roomType = RoomDetailsType.Room,
|
roomType = RoomDetailsType.Room,
|
||||||
|
roomMemberDetailsState = null,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.roomdetails.impl.members.details.BlockSection
|
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||||
|
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -135,10 +136,11 @@ fun RoomDetailsView(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
is RoomDetailsType.Dm -> {
|
is RoomDetailsType.Dm -> {
|
||||||
BlockSection(
|
if (state.roomMemberDetailsState != null) {
|
||||||
isBlocked = state.roomType.roomMember.isIgnored,
|
val roomMemberState = state.roomMemberDetailsState
|
||||||
onToggleBlock = { /*TODO*/ }
|
BlockUserSection(roomMemberState)
|
||||||
)
|
BlockUserDialogs(roomMemberState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
|
@ -28,7 +27,6 @@ import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
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.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -44,22 +42,14 @@ interface RoomMemberBindsModule {
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
object RoomMemberProvidesModule {
|
object RoomMemberProvidesModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun provideRoomDetailsPresenter(
|
|
||||||
matrixClient: MatrixClient,
|
|
||||||
room: MatrixRoom,
|
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
|
||||||
): RoomDetailsPresenter {
|
|
||||||
return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoomMemberDetailsPresenterFactory(
|
fun provideRoomMemberDetailsPresenterFactory(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
): RoomMemberDetailsPresenter.Factory {
|
): RoomMemberDetailsPresenter.Factory {
|
||||||
return object : RoomMemberDetailsPresenter.Factory {
|
return object : RoomMemberDetailsPresenter.Factory {
|
||||||
override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
|
override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
|
||||||
return RoomMemberDetailsPresenter(room, roomMember)
|
return RoomMemberDetailsPresenter(matrixClient.sessionId, room, roomMember)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ import io.element.android.libraries.di.RoomScope
|
||||||
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.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -37,6 +40,7 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val presenter: RoomMemberListPresenter,
|
private val presenter: RoomMemberListPresenter,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
|
|
@ -45,8 +49,8 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
||||||
private fun onUserSelected(matrixUser: MatrixUser) {
|
private fun onUserSelected(matrixUser: MatrixUser) = coroutineScope.launch {
|
||||||
val member = room.getMember(matrixUser.id)
|
val member = room.getMember(matrixUser.id).firstOrNull()
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
callbacks.forEach { it.openRoomMemberDetails(member) }
|
callbacks.forEach { it.openRoomMemberDetails(member) }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
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.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomUserListDataSource @Inject constructor(
|
class RoomUserListDataSource @Inject constructor(
|
||||||
|
|
@ -30,7 +31,7 @@ class RoomUserListDataSource @Inject constructor(
|
||||||
) : UserListDataSource {
|
) : UserListDataSource {
|
||||||
|
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
override suspend fun search(query: String): List<MatrixUser> {
|
||||||
return room.members().filter { member ->
|
return room.members().firstOrNull().orEmpty().filter { member ->
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members.details
|
package io.element.android.features.roomdetails.impl.members.details
|
||||||
|
|
||||||
sealed interface RoomMemberDetailsEvents
|
sealed interface RoomMemberDetailsEvents {
|
||||||
|
data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
|
||||||
|
data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
|
||||||
|
object ClearConfirmationDialog : RoomMemberDetailsEvents
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -52,7 +51,6 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
fun onShareUser() {
|
fun onShareUser() {
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,26 @@
|
||||||
package io.element.android.features.roomdetails.impl.members.details
|
package io.element.android.features.roomdetails.impl.members.details
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
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.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
|
private val currentUserSessionId: SessionId,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
@Assisted private val roomMember: RoomMember,
|
@Assisted private val roomMember: RoomMember,
|
||||||
) : Presenter<RoomMemberDetailsState> {
|
) : Presenter<RoomMemberDetailsState> {
|
||||||
|
|
@ -36,11 +47,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberDetailsState {
|
override fun present(): RoomMemberDetailsState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||||
|
var isBlocked = remember { mutableStateOf(roomMember.isIgnored) }
|
||||||
|
|
||||||
// fun handleEvents(event: RoomMemberDetailsEvents) {
|
fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||||
// when (event) {
|
when (event) {
|
||||||
// }
|
is RoomMemberDetailsEvents.BlockUser -> {
|
||||||
// }
|
if (event.needsConfirmation) {
|
||||||
|
confirmationDialog = ConfirmationDialog.Block
|
||||||
|
} else {
|
||||||
|
confirmationDialog = null
|
||||||
|
coroutineScope.blockUser(roomMember.userId, isBlocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is RoomMemberDetailsEvents.UnblockUser -> {
|
||||||
|
if (event.needsConfirmation) {
|
||||||
|
confirmationDialog = ConfirmationDialog.Unblock
|
||||||
|
} else {
|
||||||
|
confirmationDialog = null
|
||||||
|
coroutineScope.unblockUser(roomMember.userId, isBlocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val userName by produceState(initialValue = roomMember.displayName) {
|
val userName by produceState(initialValue = roomMember.displayName) {
|
||||||
room.userDisplayName(roomMember.userId).onSuccess { displayName ->
|
room.userDisplayName(roomMember.userId).onSuccess { displayName ->
|
||||||
|
|
@ -58,8 +89,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
userId = roomMember.userId.value,
|
userId = roomMember.userId.value,
|
||||||
userName = userName,
|
userName = userName,
|
||||||
avatarUrl = userAvatar,
|
avatarUrl = userAvatar,
|
||||||
isBlocked = roomMember.isIgnored,
|
isBlocked = isBlocked.value,
|
||||||
// eventSink = ::handleEvents
|
displayConfirmationDialog = confirmationDialog,
|
||||||
|
isCurrentUser = roomMember.userId == currentUserSessionId,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch {
|
||||||
|
room.ignoreUser(userId).onSuccess { isBlockedState.value = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch {
|
||||||
|
room.unignoreUser(userId).onSuccess { isBlockedState.value = false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,11 @@ data class RoomMemberDetailsState(
|
||||||
val userName: String?,
|
val userName: String?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val isBlocked: Boolean,
|
val isBlocked: Boolean,
|
||||||
// val eventSink: (RoomMemberDetailsEvents) -> Unit
|
val displayConfirmationDialog: ConfirmationDialog? = null,
|
||||||
)
|
val isCurrentUser: Boolean,
|
||||||
|
val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||||
|
) {
|
||||||
|
enum class ConfirmationDialog {
|
||||||
|
Block, Unblock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberD
|
||||||
aRoomMemberDetailsState(),
|
aRoomMemberDetailsState(),
|
||||||
aRoomMemberDetailsState().copy(userName = null),
|
aRoomMemberDetailsState().copy(userName = null),
|
||||||
aRoomMemberDetailsState().copy(isBlocked = true),
|
aRoomMemberDetailsState().copy(isBlocked = true),
|
||||||
|
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
||||||
|
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
||||||
// Add other states here
|
// Add other states here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -33,5 +35,6 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
||||||
userName = "Daniel",
|
userName = "Daniel",
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
isBlocked = false,
|
isBlocked = false,
|
||||||
// eventSink = {},
|
isCurrentUser = false,
|
||||||
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,15 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||||
|
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -86,9 +89,10 @@ fun RoomMemberDetailsView(
|
||||||
// TODO implement send DM
|
// TODO implement send DM
|
||||||
})
|
})
|
||||||
|
|
||||||
BlockSection(isBlocked = state.isBlocked, onToggleBlock = {
|
if (!state.isCurrentUser) {
|
||||||
// TODO implement block & unblock
|
BlockUserSection(state)
|
||||||
})
|
BlockUserDialogs(state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,24 +143,6 @@ internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) {
|
|
||||||
PreferenceCategory(showDivider = false, modifier = modifier) {
|
|
||||||
if (isBlocked) {
|
|
||||||
PreferenceText(
|
|
||||||
title = stringResource(R.string.screen_dm_details_unblock_user),
|
|
||||||
icon = Icons.Outlined.Block,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PreferenceText(
|
|
||||||
title = stringResource(R.string.screen_dm_details_block_user),
|
|
||||||
icon = Icons.Outlined.Block,
|
|
||||||
tintColor = LocalColors.current.textActionCritical,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.impl.LeaveRoomWarning
|
import io.element.android.features.roomdetails.impl.LeaveRoomWarning
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||||
|
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
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.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -32,8 +33,8 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
|
@ -50,7 +51,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state is created from room info`() = runTest {
|
fun `present - initial state is created from room info`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -69,7 +70,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -84,7 +85,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state with no room name`() = runTest {
|
fun `present - initial state with no room name`() = runTest {
|
||||||
val room = aMatrixRoom(name = null)
|
val room = aMatrixRoom(name = null)
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -95,12 +96,33 @@ class RoomDetailsPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state with DM member sets custom DM roomType`() = runTest {
|
||||||
|
val room = aMatrixRoom(name = null).apply {
|
||||||
|
givenDmMember(aRoomMember())
|
||||||
|
}
|
||||||
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
// It's not configured yet in the first iteration
|
||||||
|
Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room)
|
||||||
|
|
||||||
|
// Once updated, the RoomDetailsType becomes 'Dm'
|
||||||
|
val updatedState = awaitItem()
|
||||||
|
Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(aRoomMember()))
|
||||||
|
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - can handle error while fetching member count`() = runTest {
|
fun `present - can handle error while fetching member count`() = runTest {
|
||||||
val room = aMatrixRoom(name = null).apply {
|
val room = aMatrixRoom(name = null).apply {
|
||||||
givenFetchMemberResult(Result.failure(Throwable()))
|
givenFetchMemberResult(Result.failure(Throwable()))
|
||||||
}
|
}
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -114,7 +136,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
|
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
|
||||||
val room = aMatrixRoom(isPublic = false)
|
val room = aMatrixRoom(isPublic = false)
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -131,7 +153,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
|
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
|
||||||
val room = aMatrixRoom(members = listOf(aRoomMember()))
|
val room = aMatrixRoom(members = listOf(aRoomMember()))
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -148,7 +170,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Leave with confirmation shows a generic warning`() = runTest {
|
fun `present - Leave with confirmation shows a generic warning`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -165,7 +187,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Leave without confirmation leaves the room`() = runTest {
|
fun `present - Leave without confirmation leaves the room`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -189,7 +211,7 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom().apply {
|
val room = aMatrixRoom().apply {
|
||||||
givenLeaveRoomError(Throwable())
|
givenLeaveRoomError(Throwable())
|
||||||
}
|
}
|
||||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.aMatrixRoom
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.aRoomMember
|
import io.element.android.features.roomdetails.aRoomMember
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -37,7 +40,7 @@ class RoomMemberDetailsPresenterTests {
|
||||||
givenUserAvatarUrlResult(Result.success("A custom avatar"))
|
givenUserAvatarUrlResult(Result.success("A custom avatar"))
|
||||||
}
|
}
|
||||||
val roomMember = aRoomMember(displayName = "Alice")
|
val roomMember = aRoomMember(displayName = "Alice")
|
||||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -60,7 +63,7 @@ class RoomMemberDetailsPresenterTests {
|
||||||
givenUserAvatarUrlResult(Result.failure(Throwable()))
|
givenUserAvatarUrlResult(Result.failure(Throwable()))
|
||||||
}
|
}
|
||||||
val roomMember = aRoomMember(displayName = "Alice")
|
val roomMember = aRoomMember(displayName = "Alice")
|
||||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -79,7 +82,7 @@ class RoomMemberDetailsPresenterTests {
|
||||||
givenUserAvatarUrlResult(Result.success(null))
|
givenUserAvatarUrlResult(Result.success(null))
|
||||||
}
|
}
|
||||||
val roomMember = aRoomMember(displayName = "Alice")
|
val roomMember = aRoomMember(displayName = "Alice")
|
||||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -90,4 +93,63 @@ class RoomMemberDetailsPresenterTests {
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
|
val room = aMatrixRoom()
|
||||||
|
val roomMember = aRoomMember()
|
||||||
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
|
||||||
|
|
||||||
|
val dialogState = awaitItem()
|
||||||
|
Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Block)
|
||||||
|
|
||||||
|
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||||
|
Truth.assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||||
|
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
|
||||||
|
val room = aMatrixRoom()
|
||||||
|
val roomMember = aRoomMember()
|
||||||
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
|
||||||
|
Truth.assertThat(awaitItem().isBlocked).isTrue()
|
||||||
|
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
|
||||||
|
Truth.assertThat(awaitItem().isBlocked).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
|
val room = aMatrixRoom()
|
||||||
|
val roomMember = aRoomMember()
|
||||||
|
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
|
||||||
|
|
||||||
|
val dialogState = awaitItem()
|
||||||
|
Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Unblock)
|
||||||
|
|
||||||
|
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||||
|
Truth.assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||||
|
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.core.coroutine
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
/** Create a Flow emitting a single error event. It should be useful for tests. */
|
||||||
|
fun <T> errorFlow(throwable: Throwable) = flow<T> { throw throwable }
|
||||||
|
|
@ -36,13 +36,13 @@ interface MatrixRoom : Closeable {
|
||||||
val isDirect: Boolean
|
val isDirect: Boolean
|
||||||
val isPublic: Boolean
|
val isPublic: Boolean
|
||||||
|
|
||||||
suspend fun members(): List<RoomMember>
|
fun members() : Flow<List<RoomMember>>
|
||||||
|
|
||||||
suspend fun memberCount(): Int
|
fun updateMembers()
|
||||||
|
|
||||||
fun getMember(userId: UserId): RoomMember?
|
fun getMember(userId: UserId): Flow<RoomMember?>
|
||||||
|
|
||||||
fun getDmMember(): RoomMember?
|
fun getDmMember(): Flow<RoomMember?>
|
||||||
|
|
||||||
fun syncUpdateFlow(): Flow<Long>
|
fun syncUpdateFlow(): Flow<Long>
|
||||||
|
|
||||||
|
|
@ -62,6 +62,10 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||||
|
|
||||||
suspend fun leave(): Result<Unit>
|
suspend fun leave(): Result<Unit>
|
||||||
|
|
||||||
suspend fun acceptInvitation(): Result<Unit>
|
suspend fun acceptInvitation(): Result<Unit>
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,19 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.onSubscription
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||||
|
|
||||||
class RustMatrixRoom(
|
class RustMatrixRoom(
|
||||||
private val currentUserId: UserId,
|
private val currentUserId: UserId,
|
||||||
|
|
@ -48,37 +49,40 @@ class RustMatrixRoom(
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : MatrixRoom {
|
) : MatrixRoom {
|
||||||
|
|
||||||
private var loadMembersJob: Job? = null
|
private val timeline by lazy {
|
||||||
private var cachedMembers: List<RoomMember> = emptyList()
|
RustMatrixTimeline(
|
||||||
|
matrixRoom = this,
|
||||||
|
innerRoom = innerRoom,
|
||||||
|
slidingSyncRoom = slidingSyncRoom,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun members(): List<RoomMember> {
|
private var membersFlow = MutableStateFlow<List<RoomMember>>(emptyList())
|
||||||
return cachedMembers.ifEmpty {
|
|
||||||
if (loadMembersJob == null) {
|
override fun members(): Flow<List<RoomMember>> {
|
||||||
loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) {
|
return membersFlow.onSubscription { updateMembers() }
|
||||||
cachedMembers = tryOrNull {
|
}
|
||||||
innerRoom.members().map(RoomMemberMapper::map)
|
|
||||||
} ?: emptyList()
|
override fun updateMembers() {
|
||||||
}
|
val updatedMembers = tryOrNull {
|
||||||
|
innerRoom.members().map(RoomMemberMapper::map)
|
||||||
|
} ?: emptyList()
|
||||||
|
membersFlow.tryEmit(updatedMembers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMember(userId: UserId): Flow<RoomMember?> {
|
||||||
|
return membersFlow.map { members -> members.find { it.userId == userId } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDmMember(): Flow<RoomMember?> {
|
||||||
|
return membersFlow.map { members ->
|
||||||
|
if (members.size == 2 && isDirect && isEncrypted) {
|
||||||
|
members.find { it.userId != currentUserId }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
loadMembersJob?.join()
|
|
||||||
loadMembersJob = null
|
|
||||||
cachedMembers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun memberCount(): Int {
|
|
||||||
return members().size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMember(userId: UserId): RoomMember? {
|
|
||||||
return cachedMembers.find { it.userId == userId }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDmMember(): RoomMember? {
|
|
||||||
return if (cachedMembers.size == 2 && isDirect && isEncrypted) {
|
|
||||||
cachedMembers.find { it.userId != currentUserId }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,13 +98,7 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun timeline(): MatrixTimeline {
|
override fun timeline(): MatrixTimeline {
|
||||||
return RustMatrixTimeline(
|
return timeline
|
||||||
matrixRoom = this,
|
|
||||||
innerRoom = innerRoom,
|
|
||||||
slidingSyncRoom = slidingSyncRoom,
|
|
||||||
coroutineScope = coroutineScope,
|
|
||||||
coroutineDispatchers = coroutineDispatchers
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
|
@ -219,4 +217,20 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun ignoreUser(userId: UserId): Result<Unit> {
|
||||||
|
return runCatching {
|
||||||
|
getRustMember(userId)?.ignore() ?: error("No member with userId $userId exists in room $roomId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unignoreUser(userId: UserId): Result<Unit> {
|
||||||
|
return runCatching {
|
||||||
|
getRustMember(userId)?.unignore() ?: error("No member with userId $userId exists in room $roomId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRustMember(userId: UserId): RustRoomMember? {
|
||||||
|
return innerRoom.members().find { it.userId() == userId.value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,14 @@ class RustMatrixTimeline(
|
||||||
|
|
||||||
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
|
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null)
|
val settings = RoomSubscription(
|
||||||
|
requiredState = listOf(
|
||||||
|
RequiredState(key = "m.room.topic", value = ""),
|
||||||
|
RequiredState(key = "m.room.canonical_alias", value = ""),
|
||||||
|
RequiredState(key = "m.room.join_rules", value = ""),
|
||||||
|
),
|
||||||
|
timelineLimit = null
|
||||||
|
)
|
||||||
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
|
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
|
||||||
listenerTokens += result.taskHandle
|
listenerTokens += result.taskHandle
|
||||||
result.items
|
result.items
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(projects.libraries.core)
|
||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
api(libs.coroutines.core)
|
api(libs.coroutines.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.room
|
package io.element.android.libraries.matrix.test.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.coroutine.errorFlow
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
class FakeMatrixRoom(
|
class FakeMatrixRoom(
|
||||||
override val roomId: RoomId = A_ROOM_ID,
|
override val roomId: RoomId = A_ROOM_ID,
|
||||||
|
|
@ -50,6 +52,8 @@ class FakeMatrixRoom(
|
||||||
private var rejectInviteResult = Result.success(Unit)
|
private var rejectInviteResult = Result.success(Unit)
|
||||||
private var dmMember: RoomMember? = null
|
private var dmMember: RoomMember? = null
|
||||||
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
||||||
|
private var ignoreResult = Result.success(Unit)
|
||||||
|
private var unignoreResult = Result.success(Unit)
|
||||||
|
|
||||||
var areMembersFetched: Boolean = false
|
var areMembersFetched: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -78,8 +82,8 @@ class FakeMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDmMember(): RoomMember? {
|
override fun getDmMember(): Flow<RoomMember?> {
|
||||||
return dmMember
|
return flowOf(dmMember)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun userDisplayName(userId: UserId): Result<String?> {
|
override suspend fun userDisplayName(userId: UserId): Result<String?> {
|
||||||
|
|
@ -90,20 +94,18 @@ class FakeMatrixRoom(
|
||||||
return userAvatarUrlResult
|
return userAvatarUrlResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun members(): List<RoomMember> {
|
override fun members(): Flow<List<RoomMember>> {
|
||||||
return members
|
return fetchMemberResult.fold(onSuccess = {
|
||||||
|
flowOf(members)
|
||||||
|
}, onFailure = {
|
||||||
|
errorFlow(it)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun memberCount(): Int {
|
override fun updateMembers() = Unit
|
||||||
if (fetchMemberResult.isSuccess) {
|
|
||||||
return members.count()
|
|
||||||
} else {
|
|
||||||
throw fetchMemberResult.exceptionOrNull()!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMember(userId: UserId): RoomMember? {
|
override fun getMember(userId: UserId): Flow<RoomMember?> {
|
||||||
return members.firstOrNull { it.userId == userId }
|
return flowOf(members.find { it.userId == userId })
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||||
|
|
@ -138,6 +140,10 @@ class FakeMatrixRoom(
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun ignoreUser(userId: UserId): Result<Unit> = ignoreResult
|
||||||
|
|
||||||
|
override suspend fun unignoreUser(userId: UserId): Result<Unit> = unignoreResult
|
||||||
|
|
||||||
override suspend fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
|
override suspend fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||||
override suspend fun acceptInvitation(): Result<Unit> {
|
override suspend fun acceptInvitation(): Result<Unit> {
|
||||||
isInviteAccepted = true
|
isInviteAccepted = true
|
||||||
|
|
@ -179,4 +185,12 @@ class FakeMatrixRoom(
|
||||||
rejectInviteResult = result
|
rejectInviteResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun givenIgnoreResult(result: Result<Unit>) {
|
||||||
|
ignoreResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenUnIgnoreResult(result: Result<Unit>) {
|
||||||
|
unignoreResult = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
|
||||||
|
size 28744
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d
|
||||||
|
size 28744
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
|
||||||
|
size 28303
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff
|
||||||
|
size 28303
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648
|
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
|
||||||
size 67340
|
size 64550
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9
|
oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842
|
||||||
size 68135
|
size 64550
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a
|
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
|
||||||
size 61924
|
size 58643
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061
|
oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8
|
||||||
size 62356
|
size 58643
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue