[Room Details] Block & unblock user (#340)
This commit is contained in:
committed by
GitHub
parent
0d327cd543
commit
aef2b38983
@@ -18,6 +18,7 @@ package io.element.android.appnav
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
@@ -134,8 +135,18 @@ class RoomFlowNode @AssistedInject constructor(
|
||||
object RoomDetails : NavTarget
|
||||
}
|
||||
|
||||
private val timeline = inputs.room.timeline()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Children(
|
||||
navModel = backstack,
|
||||
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)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.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
|
||||
fun `present - load more`() = runTest {
|
||||
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,
|
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||
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) {
|
||||
val context = LocalContext.current
|
||||
val state = presenter.present()
|
||||
|
||||
fun onShareRoom() {
|
||||
this.onShareRoom(context)
|
||||
}
|
||||
|
||||
fun onShareMember(roomMember: RoomMember) {
|
||||
this.onShareMember(context, roomMember)
|
||||
}
|
||||
|
||||
RoomDetailsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
goBack = { navigateUp() },
|
||||
onShareRoom = { onShareRoom(context) },
|
||||
onShareMember = { onShareMember(context, it) },
|
||||
goBack = this::navigateUp,
|
||||
onShareRoom = ::onShareRoom,
|
||||
onShareMember = ::onShareMember,
|
||||
openRoomMemberList = ::openRoomMemberList,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.Presenter
|
||||
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.RoomMembershipObserver
|
||||
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.withContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomDetailsPresenter @Inject constructor(
|
||||
private val sessionId: SessionId,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val room: MatrixRoom,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
) : Presenter<RoomDetailsState> {
|
||||
|
||||
private val roomMemberDetailsPresenter by lazy {
|
||||
val dmMember = runBlocking {
|
||||
room.getDmMember().firstOrNull()
|
||||
}
|
||||
if (dmMember != null) {
|
||||
RoomMemberDetailsPresenter(matrixClient.sessionId, room, dmMember)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -50,20 +68,16 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
mutableStateOf<RoomDetailsError?>(null)
|
||||
}
|
||||
|
||||
var memberCount: Async<Int> by remember { mutableStateOf(Async.Loading()) }
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
memberCount = runCatching { room.memberCount() }
|
||||
.fold(
|
||||
onSuccess = { Async.Success(it) },
|
||||
onFailure = { Async.Failure(it) }
|
||||
)
|
||||
}
|
||||
val memberCount by produceState<Async<Int>>(initialValue = Async.Loading(null)) {
|
||||
room.members().map { it.count() }
|
||||
.onEach { value = Async.Success(it) }
|
||||
.catch { value = Async.Failure(it) }
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
val dmMember = room.getDmMember()
|
||||
val dmMember by room.getDmMember().collectAsState(initial = null)
|
||||
val roomType = if (dmMember != null) {
|
||||
RoomDetailsType.Dm(dmMember)
|
||||
RoomDetailsType.Dm(dmMember!!)
|
||||
} else {
|
||||
RoomDetailsType.Room
|
||||
}
|
||||
@@ -90,6 +104,12 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val roomMemberDetailsState = if (dmMember != null) {
|
||||
roomMemberDetailsPresenter?.present()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return RoomDetailsState(
|
||||
roomId = room.roomId.value,
|
||||
roomName = room.name ?: room.displayName,
|
||||
@@ -101,6 +121,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
displayLeaveRoomWarning = leaveRoomWarning,
|
||||
error = error,
|
||||
roomType = roomType,
|
||||
roomMemberDetailsState = roomMemberDetailsState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
|
||||
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.isLoading
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
data class RoomDetailsState(
|
||||
@@ -33,6 +31,7 @@ data class RoomDetailsState(
|
||||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||
val error: RoomDetailsError?,
|
||||
val roomType: RoomDetailsType,
|
||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
||||
@@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||
displayLeaveRoomWarning = null,
|
||||
error = null,
|
||||
roomType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.RoomMemberShareSection
|
||||
import io.element.android.libraries.architecture.Async
|
||||
@@ -135,10 +136,11 @@ fun RoomDetailsView(
|
||||
})
|
||||
}
|
||||
is RoomDetailsType.Dm -> {
|
||||
BlockSection(
|
||||
isBlocked = state.roomType.roomMember.isIgnored,
|
||||
onToggleBlock = { /*TODO*/ }
|
||||
)
|
||||
if (state.roomMemberDetailsState != null) {
|
||||
val roomMemberState = state.roomMemberDetailsState
|
||||
BlockUserSection(roomMemberState)
|
||||
BlockUserDialogs(roomMemberState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
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.details.RoomMemberDetailsPresenter
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@@ -44,22 +42,14 @@ interface RoomMemberBindsModule {
|
||||
@ContributesTo(RoomScope::class)
|
||||
object RoomMemberProvidesModule {
|
||||
|
||||
@Provides
|
||||
fun provideRoomDetailsPresenter(
|
||||
matrixClient: MatrixClient,
|
||||
room: MatrixRoom,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
): RoomDetailsPresenter {
|
||||
return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideRoomMemberDetailsPresenterFactory(
|
||||
matrixClient: MatrixClient,
|
||||
room: MatrixRoom,
|
||||
): RoomMemberDetailsPresenter.Factory {
|
||||
return object : RoomMemberDetailsPresenter.Factory {
|
||||
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.RoomMember
|
||||
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
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@@ -37,6 +40,7 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val room: MatrixRoom,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
@@ -45,8 +49,8 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
private fun onUserSelected(matrixUser: MatrixUser) {
|
||||
val member = room.getMember(matrixUser.id)
|
||||
private fun onUserSelected(matrixUser: MatrixUser) = coroutineScope.launch {
|
||||
val member = room.getMember(matrixUser.id).firstOrNull()
|
||||
if (member != null) {
|
||||
callbacks.forEach { it.openRoomMemberDetails(member) }
|
||||
} 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.RoomMember
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomUserListDataSource @Inject constructor(
|
||||
@@ -30,7 +31,7 @@ class RoomUserListDataSource @Inject constructor(
|
||||
) : UserListDataSource {
|
||||
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return room.members().filter { member ->
|
||||
return room.members().firstOrNull().orEmpty().filter { member ->
|
||||
if (query.isBlank()) {
|
||||
true
|
||||
} else {
|
||||
|
||||
@@ -16,4 +16,8 @@
|
||||
|
||||
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.inputs
|
||||
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.room.RoomMember
|
||||
import timber.log.Timber
|
||||
@@ -52,7 +51,6 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
fun onShareUser() {
|
||||
|
||||
@@ -17,15 +17,26 @@
|
||||
package io.element.android.features.roomdetails.impl.members.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog
|
||||
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.RoomMember
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
private val currentUserSessionId: SessionId,
|
||||
private val room: MatrixRoom,
|
||||
@Assisted private val roomMember: RoomMember,
|
||||
) : Presenter<RoomMemberDetailsState> {
|
||||
@@ -36,11 +47,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
var isBlocked = remember { mutableStateOf(roomMember.isIgnored) }
|
||||
|
||||
// fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||
// when (event) {
|
||||
// }
|
||||
// }
|
||||
fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||
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) {
|
||||
room.userDisplayName(roomMember.userId).onSuccess { displayName ->
|
||||
@@ -58,8 +89,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
userId = roomMember.userId.value,
|
||||
userName = userName,
|
||||
avatarUrl = userAvatar,
|
||||
isBlocked = roomMember.isIgnored,
|
||||
// eventSink = ::handleEvents
|
||||
isBlocked = isBlocked.value,
|
||||
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 avatarUrl: String?,
|
||||
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().copy(userName = null),
|
||||
aRoomMemberDetailsState().copy(isBlocked = true),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
@@ -33,5 +35,6 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
||||
userName = "Daniel",
|
||||
avatarUrl = null,
|
||||
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.PreviewParameter
|
||||
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.libraries.designsystem.ElementTextStyles
|
||||
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.AvatarSize
|
||||
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.PreferenceText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
@@ -86,9 +89,10 @@ fun RoomMemberDetailsView(
|
||||
// TODO implement send DM
|
||||
})
|
||||
|
||||
BlockSection(isBlocked = state.isBlocked, onToggleBlock = {
|
||||
// TODO implement block & unblock
|
||||
})
|
||||
if (!state.isCurrentUser) {
|
||||
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
|
||||
@Composable
|
||||
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.RoomDetailsEvent
|
||||
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.matrix.api.core.RoomId
|
||||
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.test.A_ROOM_ID
|
||||
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.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@@ -50,7 +51,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state is created from room info`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -69,7 +70,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -84,7 +85,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state with no room name`() = runTest {
|
||||
val room = aMatrixRoom(name = null)
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.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
|
||||
fun `present - can handle error while fetching member count`() = runTest {
|
||||
val room = aMatrixRoom(name = null).apply {
|
||||
givenFetchMemberResult(Result.failure(Throwable()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -114,7 +136,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom(isPublic = false)
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -131,7 +153,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom(members = listOf(aRoomMember()))
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -148,7 +170,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - Leave with confirmation shows a generic warning`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -165,7 +187,7 @@ class RoomDetailsPresenterTests {
|
||||
@Test
|
||||
fun `present - Leave without confirmation leaves the room`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -189,7 +211,7 @@ class RoomDetailsPresenterTests {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenLeaveRoomError(Throwable())
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver)
|
||||
val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
||||
@@ -22,7 +22,10 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.roomdetails.aMatrixRoom
|
||||
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.RoomMemberDetailsState
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
@@ -37,7 +40,7 @@ class RoomMemberDetailsPresenterTests {
|
||||
givenUserAvatarUrlResult(Result.success("A custom avatar"))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -60,7 +63,7 @@ class RoomMemberDetailsPresenterTests {
|
||||
givenUserAvatarUrlResult(Result.failure(Throwable()))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -79,7 +82,7 @@ class RoomMemberDetailsPresenterTests {
|
||||
givenUserAvatarUrlResult(Result.success(null))
|
||||
}
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||
val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -90,4 +93,63 @@ class RoomMemberDetailsPresenterTests {
|
||||
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 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>
|
||||
|
||||
@@ -62,6 +62,10 @@ interface MatrixRoom : Closeable {
|
||||
|
||||
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 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.impl.timeline.RustMatrixTimeline
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||
|
||||
class RustMatrixRoom(
|
||||
private val currentUserId: UserId,
|
||||
@@ -48,37 +49,40 @@ class RustMatrixRoom(
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : MatrixRoom {
|
||||
|
||||
private var loadMembersJob: Job? = null
|
||||
private var cachedMembers: List<RoomMember> = emptyList()
|
||||
private val timeline by lazy {
|
||||
RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun members(): List<RoomMember> {
|
||||
return cachedMembers.ifEmpty {
|
||||
if (loadMembersJob == null) {
|
||||
loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) {
|
||||
cachedMembers = tryOrNull {
|
||||
innerRoom.members().map(RoomMemberMapper::map)
|
||||
} ?: emptyList()
|
||||
}
|
||||
private var membersFlow = MutableStateFlow<List<RoomMember>>(emptyList())
|
||||
|
||||
override fun members(): Flow<List<RoomMember>> {
|
||||
return membersFlow.onSubscription { updateMembers() }
|
||||
}
|
||||
|
||||
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 {
|
||||
return RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
return timeline
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
listenerTokens += result.taskHandle
|
||||
result.items
|
||||
|
||||
@@ -23,6 +23,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.core)
|
||||
api(projects.libraries.matrix.api)
|
||||
api(libs.coroutines.core)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
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.RoomId
|
||||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class FakeMatrixRoom(
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
@@ -50,6 +52,8 @@ class FakeMatrixRoom(
|
||||
private var rejectInviteResult = Result.success(Unit)
|
||||
private var dmMember: RoomMember? = null
|
||||
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
||||
private var ignoreResult = Result.success(Unit)
|
||||
private var unignoreResult = Result.success(Unit)
|
||||
|
||||
var areMembersFetched: Boolean = false
|
||||
private set
|
||||
@@ -78,8 +82,8 @@ class FakeMatrixRoom(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDmMember(): RoomMember? {
|
||||
return dmMember
|
||||
override fun getDmMember(): Flow<RoomMember?> {
|
||||
return flowOf(dmMember)
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> {
|
||||
@@ -90,20 +94,18 @@ class FakeMatrixRoom(
|
||||
return userAvatarUrlResult
|
||||
}
|
||||
|
||||
override suspend fun members(): List<RoomMember> {
|
||||
return members
|
||||
override fun members(): Flow<List<RoomMember>> {
|
||||
return fetchMemberResult.fold(onSuccess = {
|
||||
flowOf(members)
|
||||
}, onFailure = {
|
||||
errorFlow(it)
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun memberCount(): Int {
|
||||
if (fetchMemberResult.isSuccess) {
|
||||
return members.count()
|
||||
} else {
|
||||
throw fetchMemberResult.exceptionOrNull()!!
|
||||
}
|
||||
}
|
||||
override fun updateMembers() = Unit
|
||||
|
||||
override fun getMember(userId: UserId): RoomMember? {
|
||||
return members.firstOrNull { it.userId == userId }
|
||||
override fun getMember(userId: UserId): Flow<RoomMember?> {
|
||||
return flowOf(members.find { it.userId == userId })
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||
@@ -138,6 +140,10 @@ class FakeMatrixRoom(
|
||||
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 acceptInvitation(): Result<Unit> {
|
||||
isInviteAccepted = true
|
||||
@@ -179,4 +185,12 @@ class FakeMatrixRoom(
|
||||
rejectInviteResult = result
|
||||
}
|
||||
|
||||
|
||||
fun givenIgnoreResult(result: Result<Unit>) {
|
||||
ignoreResult = result
|
||||
}
|
||||
|
||||
fun givenUnIgnoreResult(result: Result<Unit>) {
|
||||
unignoreResult = result
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
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