Room member moderation: kick, ban and unban (#2496)
* Room member moderation: kick, ban and unban --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
22715f4ea3
commit
d8f9408cdb
113 changed files with 1410 additions and 83 deletions
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RoomMemberListEvents {
|
||||
data class UpdateSearchQuery(val query: String) : RoomMemberListEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents
|
||||
data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,15 +35,16 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
presenterFactory: RoomMemberListPresenter.Factory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(buildContext, plugins = plugins), RoomMemberListNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun openRoomMemberDetails(roomMemberId: UserId)
|
||||
fun openInviteMembers()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
private val presenter = presenterFactory.create(this)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -53,27 +54,35 @@ class RoomMemberListNode @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
override fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
callbacks.forEach {
|
||||
it.openRoomMemberDetails(roomMemberId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInviteMembers() {
|
||||
override fun openInviteMembers() {
|
||||
callbacks.forEach {
|
||||
it.openInviteMembers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun exitRoomMemberList() {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = { navigateUp() },
|
||||
onMemberSelected = this::openRoomMemberDetails,
|
||||
onInvitePressed = this::openInviteMembers,
|
||||
navigator = this,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RoomMemberListNavigator {
|
||||
fun exitRoomMemberList() {}
|
||||
fun openRoomMemberDetails(roomMemberId: UserId) {}
|
||||
fun openInviteMembers() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,32 +23,45 @@ 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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMemberListPresenter @Inject constructor(
|
||||
class RoomMemberListPresenter @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val roomMembersModerationPresenter: RoomMembersModerationPresenter,
|
||||
@Assisted private val navigator: RoomMemberListNavigator,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var roomMembers by remember { mutableStateOf<AsyncData<RoomMembers>>(AsyncData.Loading()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults by remember {
|
||||
|
|
@ -61,18 +74,14 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
value = room.canInvite().getOrElse { false }
|
||||
}
|
||||
|
||||
val canDisplayBannedUsers by produceState(initialValue = false) {
|
||||
val roomIsNotDmAndUserCanBan = !room.isDm && room.canBan().getOrElse { false }
|
||||
if (roomIsNotDmAndUserCanBan) {
|
||||
room.membersStateFlow
|
||||
.onEach { members ->
|
||||
val hasBannedUsers = members.roomMembers()?.any { it.membership == RoomMembershipState.BAN }.orFalse()
|
||||
value = hasBannedUsers
|
||||
}
|
||||
.collect()
|
||||
} else {
|
||||
value = false
|
||||
}
|
||||
val isRoomModerationEnabled by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
}
|
||||
|
||||
val roomModerationState = if (isRoomModerationEnabled) {
|
||||
roomMembersModerationPresenter.present()
|
||||
} else {
|
||||
remember { roomMembersModerationPresenter.dummyState() }
|
||||
}
|
||||
|
||||
LaunchedEffect(membersState) {
|
||||
|
|
@ -116,19 +125,28 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomMemberListEvents) {
|
||||
when (event) {
|
||||
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch {
|
||||
if (roomMembersModerationPresenter.canDisplayModerationActions()) {
|
||||
roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember))
|
||||
} else {
|
||||
navigator.openRoomMemberDetails(event.roomMember.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
isSearchActive = isSearchActive,
|
||||
canInvite = canInvite,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = { event ->
|
||||
when (event) {
|
||||
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
}
|
||||
},
|
||||
moderationState = roomModerationState,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -27,7 +28,7 @@ data class RoomMemberListState(
|
|||
val searchResults: SearchBarResultState<RoomMembers>,
|
||||
val isSearchActive: Boolean,
|
||||
val canInvite: Boolean,
|
||||
val canDisplayBannedUsers: Boolean,
|
||||
val moderationState: RoomMembersModerationState,
|
||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -57,30 +59,20 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
searchQuery = "something-with-no-results",
|
||||
searchResults = SearchBarResultState.NoResultsFound()
|
||||
),
|
||||
aRoomMemberListState().copy(
|
||||
roomMembers = AsyncData.Success(
|
||||
RoomMembers(
|
||||
invited = persistentListOf(aVictor(), aWalter()),
|
||||
joined = persistentListOf(anAlice(), aBob(), aWalter()),
|
||||
banned = persistentListOf(),
|
||||
)
|
||||
),
|
||||
canDisplayBannedUsers = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
roomMembers: AsyncData<RoomMembers> = AsyncData.Uninitialized,
|
||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(),
|
||||
canDisplayBannedUsers: Boolean = false,
|
||||
moderationState: RoomMembersModerationState = aRoomMembersModerationState(),
|
||||
) = RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = "",
|
||||
searchResults = searchResults,
|
||||
isSearchActive = false,
|
||||
canInvite = false,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
moderationState = moderationState,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -46,6 +47,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
|
|
@ -76,14 +79,12 @@ private enum class SelectedSection {
|
|||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
onBackPressed: () -> Unit,
|
||||
onInvitePressed: () -> Unit,
|
||||
onMemberSelected: (UserId) -> Unit,
|
||||
navigator: RoomMemberListNavigator,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSelectedSectionIndex: Int = 0,
|
||||
) {
|
||||
fun onUserSelected(roomMember: RoomMember) {
|
||||
onMemberSelected(roomMember.userId)
|
||||
state.eventSink(RoomMemberListEvents.RoomMemberSelected(roomMember))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -92,13 +93,18 @@ fun RoomMemberListView(
|
|||
if (!state.isSearchActive) {
|
||||
RoomMemberListTopBar(
|
||||
canInvite = state.canInvite,
|
||||
onBackPressed = onBackPressed,
|
||||
onInvitePressed = onInvitePressed,
|
||||
onBackPressed = navigator::exitRoomMemberList,
|
||||
onInvitePressed = navigator::openInviteMembers,
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) }
|
||||
if (!state.moderationState.canDisplayBannedUsers && selectedSection == SelectedSection.BANNED) {
|
||||
SideEffect {
|
||||
selectedSection = SelectedSection.MEMBERS
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -123,7 +129,7 @@ fun RoomMemberListView(
|
|||
RoomMemberList(
|
||||
roomMembers = state.roomMembers.data,
|
||||
showMembersCount = true,
|
||||
canDisplayBannedUsersControls = state.canDisplayBannedUsers,
|
||||
canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers,
|
||||
selectedSection = selectedSection,
|
||||
onSelectedSectionChanged = { selectedSection = it },
|
||||
onUserSelected = ::onUserSelected,
|
||||
|
|
@ -136,6 +142,11 @@ fun RoomMemberListView(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoomMembersModerationView(
|
||||
state = state.moderationState,
|
||||
onDisplayMemberProfile = navigator::openRoomMemberDetails
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -328,9 +339,7 @@ private fun RoomMemberSearchBar(
|
|||
internal fun RoomMemberListPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview {
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
onMemberSelected = {},
|
||||
onInvitePressed = {},
|
||||
navigator = object : RoomMemberListNavigator {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -351,10 +360,8 @@ internal fun RoomMemberBannedListPreview() = ElementPreview {
|
|||
),
|
||||
)
|
||||
),
|
||||
canDisplayBannedUsers = true,
|
||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||
),
|
||||
onBackPressed = {},
|
||||
onMemberSelected = {},
|
||||
onInvitePressed = {},
|
||||
navigator = object : RoomMemberListNavigator {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
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 com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.finally
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultRoomMembersModerationPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : RoomMembersModerationPresenter {
|
||||
private var selectedMember by mutableStateOf<RoomMember?>(null)
|
||||
|
||||
private suspend fun canBan() = room.canBan().getOrDefault(false)
|
||||
private suspend fun canKick() = room.canKick().getOrDefault(false)
|
||||
|
||||
override suspend fun canDisplayModerationActions(): Boolean {
|
||||
val isRoomModerationEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
val isDm = room.isDm && room.isEncrypted
|
||||
return isRoomModerationEnabled && !isDm && (canBan() || canKick())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMembersModerationState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
|
||||
|
||||
val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
|
||||
val canDisplayBannedUsers by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) && !room.isDm && canBan()
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomMembersModerationEvents) {
|
||||
when (event) {
|
||||
is RoomMembersModerationEvents.SelectRoomMember -> {
|
||||
coroutineScope.launch {
|
||||
selectedMember = event.roomMember
|
||||
if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) {
|
||||
unbanUserAsyncAction.value = AsyncAction.Confirming
|
||||
} else {
|
||||
moderationActions = buildList {
|
||||
add(ModerationAction.DisplayProfile(event.roomMember.userId))
|
||||
val currentUserMemberPowerLevel = room.userRole(room.sessionId).getOrDefault(RoomMember.Role.USER).powerLevel
|
||||
if (currentUserMemberPowerLevel > event.roomMember.powerLevel) {
|
||||
if (canKick()) {
|
||||
add(ModerationAction.KickUser(event.roomMember.userId))
|
||||
}
|
||||
if (canBan()) {
|
||||
add(ModerationAction.BanUser(event.roomMember.userId))
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.KickUser -> {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.kickUser(it.userId, kickUserAsyncAction)
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.BanUser -> {
|
||||
if (banUserAsyncAction.value.isConfirming()) {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.banUser(it.userId, banUserAsyncAction)
|
||||
}
|
||||
} else {
|
||||
banUserAsyncAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.UnbanUser -> {
|
||||
if (unbanUserAsyncAction.value.isConfirming()) {
|
||||
moderationActions = persistentListOf()
|
||||
selectedMember?.let {
|
||||
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
|
||||
}
|
||||
} else {
|
||||
unbanUserAsyncAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
is RoomMembersModerationEvents.Reset -> {
|
||||
selectedMember = null
|
||||
moderationActions = persistentListOf()
|
||||
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
banUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomMembersModerationState(
|
||||
selectedRoomMember = selectedMember,
|
||||
actions = moderationActions,
|
||||
kickUserAsyncAction = kickUserAsyncAction.value,
|
||||
banUserAsyncAction = banUserAsyncAction.value,
|
||||
unbanUserAsyncAction = unbanUserAsyncAction.value,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.kickUser(
|
||||
userId: UserId,
|
||||
kickUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(kickUserAction) {
|
||||
room.kickUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.banUser(
|
||||
userId: UserId,
|
||||
banUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(banUserAction) {
|
||||
room.banUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.unbanUser(
|
||||
userId: UserId,
|
||||
unbanUserAction: MutableState<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||
room.unbanUser(userId).finally { selectedMember = null }
|
||||
}
|
||||
|
||||
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState<AsyncAction<T>>, block: suspend () -> Result<T>) {
|
||||
launch(dispatchers.io) {
|
||||
action.runUpdatingState {
|
||||
val result = block()
|
||||
if (result.isSuccess) {
|
||||
room.membersStateFlow.drop(1).take(1)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RoomMembersModerationEvents {
|
||||
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
|
||||
data object KickUser : RoomMembersModerationEvents
|
||||
data object BanUser : RoomMembersModerationEvents
|
||||
data object UnbanUser : RoomMembersModerationEvents
|
||||
data object Reset : RoomMembersModerationEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
interface RoomMembersModerationPresenter : Presenter<RoomMembersModerationState> {
|
||||
suspend fun canDisplayModerationActions(): Boolean
|
||||
|
||||
fun dummyState() = RoomMembersModerationState(
|
||||
selectedRoomMember = null,
|
||||
actions = persistentListOf(),
|
||||
kickUserAsyncAction = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction = AsyncAction.Uninitialized,
|
||||
canDisplayBannedUsers = false,
|
||||
eventSink = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomMembersModerationState(
|
||||
val selectedRoomMember: RoomMember?,
|
||||
val actions: ImmutableList<ModerationAction>,
|
||||
val kickUserAsyncAction: AsyncAction<Unit>,
|
||||
val banUserAsyncAction: AsyncAction<Unit>,
|
||||
val unbanUserAsyncAction: AsyncAction<Unit>,
|
||||
val canDisplayBannedUsers: Boolean,
|
||||
val eventSink: (RoomMembersModerationEvents) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface ModerationAction {
|
||||
data class DisplayProfile(val userId: UserId) : ModerationAction
|
||||
data class KickUser(val userId: UserId) : ModerationAction
|
||||
data class BanUser(val userId: UserId) : ModerationAction
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider<RoomMembersModerationState> {
|
||||
override val values: Sequence<RoomMembersModerationState>
|
||||
get() = sequenceOf(
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = persistentListOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = persistentListOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
ModerationAction.KickUser(userId = anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = persistentListOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
ModerationAction.KickUser(userId = anAlice().userId),
|
||||
ModerationAction.BanUser(userId = anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
kickUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
unbanUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
kickUserAsyncAction = AsyncAction.Failure(Exception("Failed to kick user")),
|
||||
banUserAsyncAction = AsyncAction.Failure(Exception("Failed to ban user")),
|
||||
unbanUserAsyncAction = AsyncAction.Failure(Exception("Failed to unban user")),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
unbanUserAsyncAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
kickUserAsyncAction = AsyncAction.Success(Unit),
|
||||
banUserAsyncAction = AsyncAction.Success(Unit),
|
||||
unbanUserAsyncAction = AsyncAction.Success(Unit),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomMembersModerationState(
|
||||
selectedRoomMember: RoomMember? = null,
|
||||
actions: List<ModerationAction> = emptyList(),
|
||||
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canDisplayBannedUsers: Boolean = false,
|
||||
eventSink: (RoomMembersModerationEvents) -> Unit = {},
|
||||
) = RoomMembersModerationState(
|
||||
selectedRoomMember = selectedRoomMember,
|
||||
actions = actions.toPersistentList(),
|
||||
kickUserAsyncAction = kickUserAsyncAction,
|
||||
banUserAsyncAction = banUserAsyncAction,
|
||||
unbanUserAsyncAction = unbanUserAsyncAction,
|
||||
canDisplayBannedUsers = canDisplayBannedUsers,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.impl.members.moderation
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
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.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.sheetStateForPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun RoomMembersModerationView(
|
||||
state: RoomMembersModerationState,
|
||||
onDisplayMemberProfile: (UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
if (state.actions.isNotEmpty()) {
|
||||
RoomMemberActionsBottomSheet(
|
||||
roomMember = state.selectedRoomMember,
|
||||
actions = state.actions,
|
||||
onActionSelected = { action ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> {
|
||||
onDisplayMemberProfile(action.userId)
|
||||
}
|
||||
is ModerationAction.KickUser -> {
|
||||
state.eventSink(RoomMembersModerationEvents.KickUser)
|
||||
}
|
||||
is ModerationAction.BanUser -> {
|
||||
state.eventSink(RoomMembersModerationEvents.BanUser)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||
)
|
||||
}
|
||||
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
|
||||
|
||||
when (val action = state.kickUserAsyncAction) {
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_removing_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to kick user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (val action = state.banUserAsyncAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
|
||||
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
|
||||
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
|
||||
onSubmitClicked = { state.selectedRoomMember?.userId?.let { state.eventSink(RoomMembersModerationEvents.BanUser) } },
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }
|
||||
)
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_banning_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to ban user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (val action = state.unbanUserAsyncAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
state.selectedRoomMember?.let {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_member_list_manage_member_unban_title),
|
||||
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
|
||||
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
|
||||
onSubmitClicked = { state.eventSink(RoomMembersModerationEvents.UnbanUser) },
|
||||
onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
Timber.e(action.error, "Failed to unban user.")
|
||||
LaunchedEffect(action) {
|
||||
asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(
|
||||
text = stringResource(CommonStrings.common_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(action) { asyncIndicatorState.clear() }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomMemberActionsBottomSheet(
|
||||
roomMember: RoomMember?,
|
||||
actions: ImmutableList<ModerationAction>,
|
||||
onActionSelected: (ModerationAction) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
if (roomMember != null && actions.isNotEmpty()) {
|
||||
val bottomSheetState = if (LocalInspectionMode.current) {
|
||||
sheetStateForPreview()
|
||||
} else {
|
||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
}
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = roomMember.userId.value,
|
||||
name = roomMember.displayName,
|
||||
url = roomMember.avatarUrl,
|
||||
size = AvatarSize.RoomListManageUser,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(bottom = 28.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
roomMember.displayName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = roomMember.userId.toString(),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
for (action in actions) {
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_user_info)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
onActionSelected(action)
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is ModerationAction.KickUser -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is ModerationAction.BanUser -> {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStatePreviewProvider::class) state: RoomMembersModerationState) {
|
||||
ElementPreview {
|
||||
Box(modifier = Modifier.fillMaxWidth().heightIn(min = 64.dp)) {
|
||||
RoomMembersModerationView(
|
||||
state = state,
|
||||
onDisplayMemberProfile = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue