Merge branch 'develop' into feature/fga/room_list_filter_iteration
This commit is contained in:
commit
bdf2a45ad3
368 changed files with 6450 additions and 2317 deletions
|
|
@ -36,6 +36,7 @@ import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
|||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
|
||||
import io.element.android.features.roomdetails.impl.members.details.avatar.AvatarPreviewNode
|
||||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -91,6 +92,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object PollHistory : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AdminSettings : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -120,6 +124,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
override fun openPollHistory() {
|
||||
backstack.push(NavTarget.PollHistory)
|
||||
}
|
||||
|
||||
override fun openAdminSettings() {
|
||||
backstack.push(NavTarget.AdminSettings)
|
||||
}
|
||||
}
|
||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||
}
|
||||
|
|
@ -189,6 +197,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
is NavTarget.PollHistory -> {
|
||||
pollHistoryEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
|
||||
is NavTarget.AdminSettings -> {
|
||||
createNode<RolesAndPermissionsFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openRoomNotificationSettings()
|
||||
fun openAvatarPreview(name: String, url: String)
|
||||
fun openPollHistory()
|
||||
fun openAdminSettings()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
|
@ -119,6 +120,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.openAvatarPreview(name, url) }
|
||||
}
|
||||
|
||||
private fun openAdminSettings() {
|
||||
callbacks.forEach { it.openAdminSettings() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -151,6 +156,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
invitePeople = ::invitePeople,
|
||||
openAvatarPreview = ::openAvatarPreview,
|
||||
openPollHistory = ::openPollHistory,
|
||||
openAdminSettings = this::openAdminSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -71,6 +72,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val isUserAdmin = room.isOwnUserAdmin()
|
||||
|
||||
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
||||
|
||||
|
|
@ -78,6 +80,10 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
|
||||
val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } }
|
||||
|
||||
val isRoomModerationEnabled by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||
if (canShowNotificationSettings.value) {
|
||||
|
|
@ -150,6 +156,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
leaveRoomState = leaveRoomState,
|
||||
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
||||
isFavorite = isFavorite,
|
||||
displayRolesAndPermissionsSettings = isRoomModerationEnabled && !room.isDm && isUserAdmin,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ data class RoomDetailsState(
|
|||
val leaveRoomState: LeaveRoomState,
|
||||
val roomNotificationSettings: RoomNotificationSettings?,
|
||||
val isFavorite: Boolean,
|
||||
val displayRolesAndPermissionsSettings: Boolean,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
|||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
get() = sequenceOf(
|
||||
aRoomDetailsState(),
|
||||
aRoomDetailsState(displayAdminSettings = true),
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.Hidden),
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic),
|
||||
aRoomDetailsState(isEncrypted = false),
|
||||
|
|
@ -92,6 +92,7 @@ fun aRoomDetailsState(
|
|||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(),
|
||||
isFavorite: Boolean = false,
|
||||
displayAdminSettings: Boolean = false,
|
||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||
) = RoomDetailsState(
|
||||
roomId = roomId,
|
||||
|
|
@ -109,6 +110,7 @@ fun aRoomDetailsState(
|
|||
leaveRoomState = leaveRoomState,
|
||||
roomNotificationSettings = roomNotificationSettings,
|
||||
isFavorite = isFavorite,
|
||||
displayRolesAndPermissionsSettings = displayAdminSettings,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ fun RoomDetailsView(
|
|||
invitePeople: () -> Unit,
|
||||
openAvatarPreview: (name: String, url: String) -> Unit,
|
||||
openPollHistory: () -> Unit,
|
||||
openAdminSettings: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onShareMember() {
|
||||
|
|
@ -160,30 +161,45 @@ fun RoomDetailsView(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.canShowNotificationSettings && state.roomNotificationSettings != null) {
|
||||
NotificationSection(
|
||||
isDefaultMode = state.roomNotificationSettings.isDefault,
|
||||
openRoomNotificationSettings = openRoomNotificationSettings
|
||||
PreferenceCategory {
|
||||
if (state.canShowNotificationSettings && state.roomNotificationSettings != null) {
|
||||
NotificationItem(
|
||||
isDefaultMode = state.roomNotificationSettings.isDefault,
|
||||
openRoomNotificationSettings = openRoomNotificationSettings
|
||||
)
|
||||
}
|
||||
|
||||
FavoriteItem(
|
||||
isFavorite = state.isFavorite,
|
||||
onFavoriteChanges = {
|
||||
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||
}
|
||||
)
|
||||
|
||||
if (state.displayRolesAndPermissionsSettings) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Roles and permissions") },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
onClick = openAdminSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FavoriteSection(
|
||||
isFavorite = state.isFavorite,
|
||||
onFavoriteChanges = {
|
||||
state.eventSink(RoomDetailsEvent.SetFavorite(it))
|
||||
}
|
||||
)
|
||||
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
MembersSection(
|
||||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
|
||||
if (state.canInvite) {
|
||||
InviteSection(
|
||||
invitePeople = invitePeople
|
||||
)
|
||||
val displayMemberListItem = state.roomType is RoomDetailsType.Room
|
||||
val displayInviteMembersItem = state.canInvite
|
||||
if (displayMemberListItem || displayInviteMembersItem) {
|
||||
PreferenceCategory {
|
||||
if (displayMemberListItem) {
|
||||
MembersItem(
|
||||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
if (displayInviteMembersItem) {
|
||||
InviteItem(
|
||||
invitePeople = invitePeople
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +365,7 @@ private fun TopicSection(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationSection(
|
||||
private fun NotificationItem(
|
||||
isDefaultMode: Boolean,
|
||||
openRoomNotificationSettings: () -> Unit,
|
||||
) {
|
||||
|
|
@ -358,58 +374,49 @@ private fun NotificationSection(
|
|||
} else {
|
||||
stringResource(R.string.screen_room_details_notification_mode_custom)
|
||||
}
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
onClick = openRoomNotificationSettings,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
onClick = openRoomNotificationSettings,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FavoriteSection(
|
||||
private fun FavoriteItem(
|
||||
isFavorite: Boolean,
|
||||
onFavoriteChanges: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
PreferenceCategory(modifier = modifier) {
|
||||
PreferenceSwitch(
|
||||
icon = CompoundIcons.Favourite(),
|
||||
title = stringResource(id = CommonStrings.common_favourite),
|
||||
isChecked = isFavorite,
|
||||
onCheckedChange = onFavoriteChanges
|
||||
)
|
||||
}
|
||||
PreferenceSwitch(
|
||||
icon = CompoundIcons.Favourite(),
|
||||
title = stringResource(id = CommonStrings.common_favourite),
|
||||
isChecked = isFavorite,
|
||||
onCheckedChange = onFavoriteChanges
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MembersSection(
|
||||
private fun MembersItem(
|
||||
memberCount: Long,
|
||||
openRoomMemberList: () -> Unit,
|
||||
) {
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||
onClick = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||
onClick = openRoomMemberList,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteSection(
|
||||
private fun InviteItem(
|
||||
invitePeople: () -> Unit,
|
||||
) {
|
||||
PreferenceCategory {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
onClick = invitePeople,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
onClick = invitePeople,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -481,5 +488,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
invitePeople = {},
|
||||
openAvatarPreview = { _, _ -> },
|
||||
openPollHistory = {},
|
||||
openAdminSettings = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class RoomInviteMembersPresenter @Inject constructor(
|
|||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
var showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
fetchMembers(roomMembers)
|
||||
|
|
@ -99,7 +99,7 @@ class RoomInviteMembersPresenter @Inject constructor(
|
|||
@JvmName("toggleUserInSelectedUsers")
|
||||
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
||||
value = if (value.contains(user)) {
|
||||
value.filterNot { it == user }
|
||||
value.filterNot { it.userId == user.userId }
|
||||
} else {
|
||||
value + user
|
||||
}.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -97,7 +97,7 @@ fun RoomInviteMembersView(
|
|||
)
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
@ -157,7 +157,7 @@ private fun RoomInviteMembersSearchBar(
|
|||
placeHolderTitle = placeHolderTitle,
|
||||
contentPrefix = {
|
||||
if (selectedUsers.isNotEmpty()) {
|
||||
SelectedUsersList(
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
|
|
|
|||
|
|
@ -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,98 @@
|
|||
/*
|
||||
* 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.toPersistentList
|
||||
|
||||
class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider<RoomMembersModerationState> {
|
||||
override val values: Sequence<RoomMembersModerationState>
|
||||
get() = sequenceOf(
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice().userId),
|
||||
ModerationAction.KickUser(userId = anAlice().userId),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = anAlice(),
|
||||
actions = listOf(
|
||||
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,316 @@
|
|||
/*
|
||||
* 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.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.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 = 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface RolesAndPermissionsEvents {
|
||||
data object ChangeOwnRole : RolesAndPermissionsEvents
|
||||
data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents
|
||||
data object CancelPendingAction : RolesAndPermissionsEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RolesAndPermissionsFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<RolesAndPermissionsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.AdminSettings,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object AdminSettings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AdminList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ModeratorList : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.AdminSettings -> {
|
||||
val callback = object : RolesAndPermissionsNode.Callback {
|
||||
override fun openAdminList() {
|
||||
backstack.push(NavTarget.AdminList)
|
||||
}
|
||||
|
||||
override fun openModeratorList() {
|
||||
backstack.push(NavTarget.ModeratorList)
|
||||
}
|
||||
}
|
||||
createNode<RolesAndPermissionsNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
)
|
||||
}
|
||||
is NavTarget.AdminList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Admins)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
}
|
||||
is NavTarget.ModeratorList -> {
|
||||
val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Moderators)
|
||||
createNode<ChangeRolesNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.impl.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
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.api.room.roomMembers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RolesAndPermissionsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RolesAndPermissionsPresenter,
|
||||
private val room: MatrixRoom,
|
||||
) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun openAdminList()
|
||||
fun openModeratorList()
|
||||
}
|
||||
|
||||
private val callback = plugins<Callback>().first()
|
||||
|
||||
override fun onBackPressed() = navigateUp()
|
||||
override fun openAdminList() = callback.openAdminList()
|
||||
override fun openModeratorList() = callback.openModeratorList()
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
// Reload members when the user sees this screen
|
||||
lifecycle.addObserver(object : LifecycleEventObserver {
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
lifecycleScope.launch { room.updateMembers() }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If the user is not an admin anymore, exit this section since they won't have permissions to use it
|
||||
lifecycleScope.launch {
|
||||
room.membersStateFlow
|
||||
.map { state ->
|
||||
state.roomMembers().orEmpty().find { it.userId == room.sessionId }
|
||||
}
|
||||
.filter { it?.role != RoomMember.Role.ADMIN }
|
||||
.take(1)
|
||||
.onEach { navigateUp() }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
roomDetailsAdminSettingsNavigator = this,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface RoomDetailsAdminSettingsNavigator {
|
||||
fun onBackPressed() {}
|
||||
fun openAdminList() {}
|
||||
fun openModeratorList() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class RolesAndPermissionsPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Presenter<RolesAndPermissionsState> {
|
||||
@Composable
|
||||
override fun present(): RolesAndPermissionsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val moderatorCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.MODERATOR)
|
||||
}
|
||||
}
|
||||
val adminCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.ADMIN)
|
||||
}
|
||||
}
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: RolesAndPermissionsEvents) {
|
||||
when (event) {
|
||||
is RolesAndPermissionsEvents.ChangeOwnRole -> {
|
||||
changeOwnRoleAction.value = AsyncAction.Confirming
|
||||
}
|
||||
is RolesAndPermissionsEvents.CancelPendingAction -> {
|
||||
changeOwnRoleAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo(
|
||||
role = event.role,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return RolesAndPermissionsState(
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction.value,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.demoteSelfTo(
|
||||
role: RoomMember.Role,
|
||||
changeOwnRoleAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
runUpdatingState(changeOwnRoleAction) {
|
||||
room.updateUsersRoles(listOf(UserRoleChange(room.sessionId, role)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
|
||||
return if (this != null) {
|
||||
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class RolesAndPermissionsState(
|
||||
val adminCount: Int,
|
||||
val moderatorCount: Int,
|
||||
val changeOwnRoleAction: AsyncAction<Unit>,
|
||||
val eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
class RolesAndPermissionsStateProvider : PreviewParameterProvider<RolesAndPermissionsState> {
|
||||
override val values: Sequence<RolesAndPermissionsState>
|
||||
get() = sequenceOf(
|
||||
aRolesAndPermissionsState(),
|
||||
aRolesAndPermissionsState(adminCount = 1, moderatorCount = 2),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Confirming,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Loading,
|
||||
),
|
||||
aRolesAndPermissionsState(
|
||||
adminCount = 1,
|
||||
moderatorCount = 2,
|
||||
changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRolesAndPermissionsState(
|
||||
adminCount: Int = 0,
|
||||
moderatorCount: Int = 0,
|
||||
changeOwnRoleAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit = {},
|
||||
) = RolesAndPermissionsState(
|
||||
adminCount = adminCount,
|
||||
moderatorCount = moderatorCount,
|
||||
changeOwnRoleAction = changeOwnRoleAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
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.HorizontalDivider
|
||||
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.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun RolesAndPermissionsView(
|
||||
state: RolesAndPermissionsState,
|
||||
roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.screen_room_roles_and_permissions_title),
|
||||
onBackPressed = roomDetailsAdminSettingsNavigator::onBackPressed,
|
||||
) {
|
||||
ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
trailingContent = ListItemContent.Text("${state.adminCount}"),
|
||||
onClick = { roomDetailsAdminSettingsNavigator.openAdminList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
trailingContent = ListItemContent.Text("${state.moderatorCount}"),
|
||||
onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() },
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) },
|
||||
onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit()))
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
when (state.changeOwnRoleAction) {
|
||||
is AsyncAction.Confirming -> {
|
||||
ChangeOwnRoleBottomSheet(
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChangeOwnRoleBottomSheet(
|
||||
eventSink: (RolesAndPermissionsEvents) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = if (LocalInspectionMode.current) {
|
||||
sheetStateForPreview()
|
||||
} else {
|
||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
}
|
||||
fun dismiss() {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
}
|
||||
}
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::dismiss,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
text = stringResource(R.string.screen_room_roles_and_permissions_change_my_role),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 16.dp),
|
||||
text = stringResource(R.string.screen_room_change_role_confirm_demote_self_description),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.USER))
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_cancel)) },
|
||||
onClick = {
|
||||
sheetState.hide(coroutineScope) {
|
||||
eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
}
|
||||
},
|
||||
style = ListItemStyle.Primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) {
|
||||
ElementPreview {
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
sealed interface ChangeRolesEvent {
|
||||
data object ToggleSearchActive : ChangeRolesEvent
|
||||
data class QueryChanged(val query: String?) : ChangeRolesEvent
|
||||
data class UserSelectionToggled(val roomMember: RoomMember) : ChangeRolesEvent
|
||||
data object Save : ChangeRolesEvent
|
||||
data object Exit : ChangeRolesEvent
|
||||
data object CancelExit : ChangeRolesEvent
|
||||
data object ClearError : ChangeRolesEvent
|
||||
data object CancelSave : ChangeRolesEvent
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
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.room.RoomMember
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class ChangeRolesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ChangeRolesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
sealed interface ListType : Parcelable {
|
||||
@Parcelize
|
||||
data object Admins : ListType
|
||||
@Parcelize
|
||||
data object Moderators : ListType
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Inputs(
|
||||
val listType: ListType,
|
||||
) : NodeInputs, Parcelable
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private val presenter = presenterFactory.run {
|
||||
val role = when (inputs.listType) {
|
||||
is ListType.Admins -> RoomMember.Role.ADMIN
|
||||
is ListType.Moderators -> RoomMember.Role.MODERATOR
|
||||
}
|
||||
create(role)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
ChangeRolesView(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackPressed = this::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
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.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.PowerLevelRoomMemberComparator
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
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.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChangeRolesPresenter @AssistedInject constructor(
|
||||
@Assisted private val role: RoomMember.Role,
|
||||
private val room: MatrixRoom,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Presenter<ChangeRolesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): ChangeRolesState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val dataSource = remember { RoomMemberListDataSource(room, dispatchers) }
|
||||
var query by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
var searchResults by remember {
|
||||
mutableStateOf<SearchBarResultState<ImmutableList<RoomMember>>>(SearchBarResultState.Initial())
|
||||
}
|
||||
val selectedUsers = remember {
|
||||
mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf())
|
||||
}
|
||||
val exitState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val saveState: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val usersWithRole = produceState(initialValue = persistentListOf()) {
|
||||
room.usersWithRole(role)
|
||||
.map { members -> members.map { it.toMatrixUser() } }
|
||||
.onEach { users ->
|
||||
val previous: PersistentList<MatrixUser> = value
|
||||
value = users.toPersistentList()
|
||||
// Users who were selected but didn't have the role, so their role change was pending
|
||||
val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } }
|
||||
// Users who no longer have the role
|
||||
val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }
|
||||
selectedUsers.value = (users + toAdd - toRemove).toImmutableList()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val roomMemberState by room.membersStateFlow.collectAsState()
|
||||
|
||||
// Update search results for every query change
|
||||
LaunchedEffect(query, roomMemberState) {
|
||||
val results = dataSource
|
||||
.search(query.orEmpty())
|
||||
.sorted()
|
||||
|
||||
searchResults = if (results.isEmpty()) {
|
||||
SearchBarResultState.NoResultsFound()
|
||||
} else {
|
||||
SearchBarResultState.Results(results)
|
||||
}
|
||||
}
|
||||
|
||||
val hasPendingChanges = usersWithRole.value != selectedUsers.value
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
fun canChangeMemberRole(userId: UserId): Boolean {
|
||||
// An admin can't remove or demote another admin
|
||||
val powerLevel = roomInfo?.userPowerLevels?.get(userId) ?: 0L
|
||||
return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN
|
||||
}
|
||||
|
||||
fun handleEvent(event: ChangeRolesEvent) {
|
||||
when (event) {
|
||||
is ChangeRolesEvent.ToggleSearchActive -> {
|
||||
searchActive = !searchActive
|
||||
}
|
||||
is ChangeRolesEvent.QueryChanged -> {
|
||||
query = event.query
|
||||
}
|
||||
is ChangeRolesEvent.UserSelectionToggled -> {
|
||||
val newList = selectedUsers.value.toMutableList()
|
||||
val index = newList.indexOfFirst { it.userId == event.roomMember.userId }
|
||||
if (index >= 0) {
|
||||
newList.removeAt(index)
|
||||
} else {
|
||||
newList.add(event.roomMember.toMatrixUser())
|
||||
}
|
||||
selectedUsers.value = newList.toImmutableList()
|
||||
}
|
||||
is ChangeRolesEvent.Save -> {
|
||||
if (role == RoomMember.Role.ADMIN && selectedUsers != usersWithRole && !saveState.value.isConfirming()) {
|
||||
// Confirm adding admin
|
||||
saveState.value = AsyncAction.Confirming
|
||||
} else if (!saveState.value.isLoading()) {
|
||||
coroutineScope.save(usersWithRole.value, selectedUsers, saveState)
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.ClearError -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.Exit -> {
|
||||
exitState.value = if (exitState.value.isUninitialized() && hasPendingChanges) {
|
||||
// Has pending changes, confirm exit
|
||||
AsyncAction.Confirming
|
||||
} else {
|
||||
// No pending changes, exit immediately
|
||||
AsyncAction.Success(Unit)
|
||||
}
|
||||
}
|
||||
is ChangeRolesEvent.CancelExit -> {
|
||||
exitState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is ChangeRolesEvent.CancelSave -> {
|
||||
saveState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChangeRolesState(
|
||||
role = role,
|
||||
query = query,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.value,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState.value,
|
||||
savingState = saveState.value,
|
||||
canChangeMemberRole = ::canChangeMemberRole,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<RoomMember>.sorted(): ImmutableList<RoomMember> {
|
||||
return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList()
|
||||
}
|
||||
|
||||
private fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
private fun CoroutineScope.save(
|
||||
usersWithRole: ImmutableList<MatrixUser>,
|
||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||
saveState: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
saveState.value = AsyncAction.Loading
|
||||
|
||||
val toAdd = selectedUsers.value - usersWithRole
|
||||
val toRemove = usersWithRole - selectedUsers.value
|
||||
|
||||
val changes: List<UserRoleChange> = buildList {
|
||||
for (selectedUser in toAdd) {
|
||||
add(UserRoleChange(selectedUser.userId, role))
|
||||
}
|
||||
for (selectedUser in toRemove) {
|
||||
add(UserRoleChange(selectedUser.userId, RoomMember.Role.USER))
|
||||
}
|
||||
}
|
||||
|
||||
room.updateUsersRoles(changes)
|
||||
.onFailure {
|
||||
saveState.value = AsyncAction.Failure(it)
|
||||
}
|
||||
.onSuccess {
|
||||
saveState.value = AsyncAction.Success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
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.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChangeRolesState(
|
||||
val role: RoomMember.Role,
|
||||
val query: String?,
|
||||
val isSearchActive: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<RoomMember>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val hasPendingChanges: Boolean,
|
||||
val exitState: AsyncAction<Unit>,
|
||||
val savingState: AsyncAction<Unit>,
|
||||
val canChangeMemberRole: (UserId) -> Boolean,
|
||||
val eventSink: (ChangeRolesEvent) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||
override val values: Sequence<ChangeRolesState>
|
||||
get() = sequenceOf(
|
||||
aChangeRolesState(),
|
||||
aChangeRolesState(role = RoomMember.Role.MODERATOR),
|
||||
aChangeRolesStateWithSelectedUsers().copy(hasPendingChanges = false),
|
||||
aChangeRolesStateWithSelectedUsers(),
|
||||
aChangeRolesStateWithSelectedUsers().copy(
|
||||
selectedUsers = aMatrixUserList().take(2).toImmutableList(),
|
||||
),
|
||||
aChangeRolesStateWithSelectedUsers().copy(
|
||||
query = "Alice",
|
||||
isSearchActive = true,
|
||||
searchResults = SearchBarResultState.Results(aRoomMemberList().take(1).toImmutableList()),
|
||||
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
||||
),
|
||||
aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.Confirming),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Confirming),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
|
||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aChangeRolesState(
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
query: String? = null,
|
||||
isSearchActive: Boolean = false,
|
||||
searchResults: SearchBarResultState<ImmutableList<RoomMember>> = SearchBarResultState.NoResultsFound(),
|
||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
hasPendingChanges: Boolean = false,
|
||||
exitState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
savingState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canRemoveMember: (UserId) -> Boolean = { true },
|
||||
) = ChangeRolesState(
|
||||
role = role,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers,
|
||||
hasPendingChanges = hasPendingChanges,
|
||||
exitState = exitState,
|
||||
savingState = savingState,
|
||||
canChangeMemberRole = canRemoveMember,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState(
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aRoomMemberList().toImmutableList()),
|
||||
hasPendingChanges = true,
|
||||
canRemoveMember = { it != UserId("@alice:server.org") },
|
||||
)
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.aRoomMember
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
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.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangeRolesView(
|
||||
state: ChangeRolesState,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val updatedOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
|
||||
BackHandler {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(ChangeRolesEvent.ToggleSearchActive)
|
||||
} else {
|
||||
state.eventSink(ChangeRolesEvent.Exit)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
topBar = {
|
||||
AnimatedVisibility(visible = !state.isSearchActive) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
val title = when (state.role) {
|
||||
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title)
|
||||
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title)
|
||||
RoomMember.Role.USER -> error("This should never be reached")
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) })
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
enabled = state.hasPendingChanges,
|
||||
onClick = { state.eventSink(ChangeRolesEvent.Save) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
placeHolderTitle = stringResource(CommonStrings.common_search_for_someone),
|
||||
query = state.query.orEmpty(),
|
||||
onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) },
|
||||
resultState = state.searchResults,
|
||||
) { members ->
|
||||
SearchResultsList(
|
||||
isSearchActive = true,
|
||||
lazyListState = lazyListState,
|
||||
searchResults = members,
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
selectedUsersList = {},
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = !state.isSearchActive,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Column {
|
||||
SearchResultsList(
|
||||
isSearchActive = false,
|
||||
lazyListState = lazyListState,
|
||||
searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: persistentListOf(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
selectedUsersList = { users ->
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
selectedUsers = users,
|
||||
onUserRemoved = {
|
||||
state.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(it.userId)))
|
||||
},
|
||||
canDeselect = { state.canChangeMemberRole(it.userId) },
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
||||
|
||||
AsyncActionView(
|
||||
async = state.exitState,
|
||||
onSuccess = { updatedOnBackPressed() },
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||
content = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||
onSubmitClicked = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CancelExit) }
|
||||
)
|
||||
},
|
||||
onErrorDismiss = { /* Cannot happen */ },
|
||||
)
|
||||
|
||||
when (state.savingState) {
|
||||
is AsyncAction.Confirming -> {
|
||||
if (state.role == RoomMember.Role.ADMIN) {
|
||||
// Confirm adding new admins dialogs
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title),
|
||||
content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description),
|
||||
onSubmitClicked = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
LaunchedEffect(state.savingState) {
|
||||
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SearchResultsList(
|
||||
isSearchActive: Boolean,
|
||||
searchResults: ImmutableList<RoomMember>,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
canRemoveMember: (UserId) -> Boolean,
|
||||
onSelectionToggled: (RoomMember) -> Unit,
|
||||
lazyListState: LazyListState,
|
||||
selectedUsersList: @Composable (ImmutableList<MatrixUser>) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
item {
|
||||
selectedUsersList(selectedUsers)
|
||||
}
|
||||
stickyHeader {
|
||||
val textResId = if (isSearchActive) {
|
||||
CommonStrings.common_search_results
|
||||
} else {
|
||||
R.string.screen_room_member_list_room_members_header_title
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(ElementTheme.colors.bgCanvasDefault)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(textResId),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
}
|
||||
items(searchResults, key = { it.userId }) { roomMember ->
|
||||
val canToggle = canRemoveMember(roomMember.userId)
|
||||
val trailingContent: @Composable (() -> Unit)? = if (canToggle) {
|
||||
{
|
||||
Checkbox(
|
||||
checked = selectedUsers.any { it.userId == roomMember.userId },
|
||||
onCheckedChange = { onSelectionToggled(roomMember) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable(enabled = canToggle, onClick = { onSelectionToggled(roomMember) }),
|
||||
matrixUser = MatrixUser(
|
||||
userId = roomMember.userId,
|
||||
displayName = roomMember.displayName,
|
||||
avatarUrl = roomMember.avatarUrl,
|
||||
),
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider::class) state: ChangeRolesState) {
|
||||
ElementPreview {
|
||||
ChangeRolesView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string>
|
||||
<string name="screen_polls_history_title">"Апытанні"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Усе"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Дадаць тэму"</string>
|
||||
<string name="screen_room_details_already_a_member">"Ужо ўдзельнік"</string>
|
||||
<string name="screen_room_details_already_invited">"Ужо запрасілі"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."</string>
|
||||
<string name="screen_polls_history_title">"Hlasování"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Pouze správci"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Vykázat lidi"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Všichni"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Pozvat lidi"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Správci a moderátoři"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Odebrat lidi"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Změnit avatar místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Změnit název místnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Změnit téma místnosti"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Odeslat zprávy"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Upravit správce"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Tuto akci nebudete moci vrátit zpět. Upravujete oprávnění uživatele, tak aby měl stejnou úroveň jako vy."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Přidat správce?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Degradovat"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Degradovat se?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Upravit moderátory"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené změny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložit změny?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Přidat téma"</string>
|
||||
<string name="screen_room_details_already_a_member">"Již členem"</string>
|
||||
<string name="screen_room_details_already_invited">"Již pozván(a)"</string>
|
||||
|
|
@ -26,25 +49,31 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Vlastní"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Výchozí"</string>
|
||||
<string name="screen_room_details_notification_title">"Oznámení"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Role a oprávnění"</string>
|
||||
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
||||
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
||||
<string name="screen_room_details_topic_title">"Téma"</string>
|
||||
<string name="screen_room_details_updating_room">"Aktualizace místnosti…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Vykázat"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Jste si jisti, že chcete vykázat tohoto člena?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"V této místnosti nejsou žádní vykázaní uživatelé."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Vykazování %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d osoba"</item>
|
||||
<item quantity="few">"%1$d osoby"</item>
|
||||
<item quantity="other">"%1$d osob"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odebrat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odebrat z místnosti"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Pouze odebrat člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Odebrat člena a zakázat mu připojení v budoucnu?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Zrušit vykázání"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"Pokud budou pozváni, budou se moci do této místnosti znovu připojit."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Zrušit vykázání uživatele"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Zobrazit informace o uživateli"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Zobrazit profil"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Vykázaní"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Členové"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Nevyřízeno"</string>
|
||||
|
|
@ -67,5 +96,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Všechny zprávy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"V této místnosti mě upozornit na"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Správci"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Změnit moji roli"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Degradovat na člena"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Degradovat na moderátora"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderování členů"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Zprávy a obsah"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderátoři"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Obnovit oprávnění"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnění ztratíte aktuální nastavení."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnovit oprávnění?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Role"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti místnosti"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Role a oprávnění"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Dein Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen wirst du möglicherweise nicht benachrichtigt."</string>
|
||||
<string name="screen_polls_history_title">"Umfragen"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Alle"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderation der Mitglieder"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Nachrichten und Inhalte"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Raumdetails"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Du vergibst das selbe Rolle, die auch Du hast. Diese Aktion kann daher nicht mehr rückgängig gemacht werden."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Als Administrator hinzufügen?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Zurückstufen"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Möchtest Du Dich selbst herabstufen?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string>
|
||||
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string>
|
||||
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
|
||||
|
|
@ -67,5 +76,14 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Benachrichtige mich in diesem Raum bei"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administratoren"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderation der Mitglieder"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Nachrichten und Inhalte"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderatoren"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Berechtigungen"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Rollen und Berechtigungen zurücksetzen"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Rollen"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Raumdetails"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Rollen und Berechtigungen"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Se ha producido un error al actualizar la configuración de notificaciones."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas."</string>
|
||||
<string name="screen_polls_history_title">"Encuestas"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Todos"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Añadir tema"</string>
|
||||
<string name="screen_room_details_already_a_member">"Ya eres miembro"</string>
|
||||
<string name="screen_room_details_already_invited">"Ya estás invitado"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur s’est produite lors de la mise à jour du paramètre de notification."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."</string>
|
||||
<string name="screen_polls_history_title">"Sondages"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Tout le monde"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Changer l’avatar du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Changer le nom du salon"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Changer le sujet du salon"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Vous ne pourrez pas annuler cette action. Vous êtes en train de promouvoir l’utilisateur pour qu’il ait le même niveau que vous."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Ajouter un administrateur ?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Rétrograder"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Vous ne pourrez pas annuler ce changement car vous vous rétrogradez, si vous êtes le dernier utilisateur privilégié du salon il sera impossible de retrouver les privilèges."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Vous rétrograder ?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Ajouter un sujet"</string>
|
||||
<string name="screen_room_details_already_a_member">"Déjà membre"</string>
|
||||
<string name="screen_room_details_already_invited">"Déjà invité(e)"</string>
|
||||
|
|
@ -26,6 +36,7 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Personnalisé"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Défaut"</string>
|
||||
<string name="screen_room_details_notification_title">"Notifications"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Rôles et autorisations"</string>
|
||||
<string name="screen_room_details_room_name_label">"Nom du salon"</string>
|
||||
<string name="screen_room_details_security_title">"Sécurité"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
||||
|
|
@ -64,5 +75,11 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Tous les messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions et mots clés uniquement"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Dans ce salon, prévenez-moi pour"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Administrateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Modérateurs"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Autorisations"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Rôles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Rôles et autorisations"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Hiba történt az értesítési beállítás frissítésekor."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string>
|
||||
<string name="screen_polls_history_title">"Szavazások"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Mindenki"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Adminisztrátor hozzáadása?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Téma hozzáadása"</string>
|
||||
<string name="screen_room_details_already_a_member">"Már tag"</string>
|
||||
<string name="screen_room_details_already_invited">"Már meghívták"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Terjadi kesalahan saat memperbarui pengaturan pemberitahuan."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."</string>
|
||||
<string name="screen_polls_history_title">"Pemungutan suara"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Semua orang"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Tambahkan topik"</string>
|
||||
<string name="screen_room_details_already_a_member">"Sudah menjadi anggota"</string>
|
||||
<string name="screen_room_details_already_invited">"Sudah diundang"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi potresti non ricevere notifiche in alcune stanze."</string>
|
||||
<string name="screen_polls_history_title">"Sondaggi"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Tutti"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Aggiungi argomento"</string>
|
||||
<string name="screen_room_details_already_a_member">"Già membro"</string>
|
||||
<string name="screen_room_details_already_invited">"Già invitato"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string>
|
||||
<string name="screen_polls_history_title">"Sondaje"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Toți"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Adăugare subiect"</string>
|
||||
<string name="screen_room_details_already_a_member">"Deja membru"</string>
|
||||
<string name="screen_room_details_already_invited">"Deja invitat"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string>
|
||||
<string name="screen_polls_history_title">"Опросы"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Только для администраторов"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Заблокировать людей"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Удалить сообщения"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Для всех"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Пригласить людей"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Администраторы и модераторы"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Удалить людей"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Изменить изображение комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Изменить название комнаты"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Сменить тему комнаты"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Отправить сообщение"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Редактировать роль администраторов"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Добавить администратора?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Понизить уровень"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Понизить свой уровень?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Редактировать роль модераторов"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"У вас есть несохраненные изменения."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Сохранить изменения?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Добавить тему"</string>
|
||||
<string name="screen_room_details_already_a_member">"Уже зарегистрирован"</string>
|
||||
<string name="screen_room_details_already_invited">"Уже приглашены"</string>
|
||||
|
|
@ -26,22 +49,31 @@
|
|||
<string name="screen_room_details_notification_mode_custom">"Пользовательский"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"По умолчанию"</string>
|
||||
<string name="screen_room_details_notification_title">"Уведомления"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Роли и разрешения"</string>
|
||||
<string name="screen_room_details_room_name_label">"Название комнаты"</string>
|
||||
<string name="screen_room_details_security_title">"Безопасность"</string>
|
||||
<string name="screen_room_details_share_room_title">"Поделиться комнатой"</string>
|
||||
<string name="screen_room_details_topic_title">"Тема"</string>
|
||||
<string name="screen_room_details_updating_room">"Обновление комнаты…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Заблокировать"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Они не смогут снова присоединиться к этой комнате, если их пригласят."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Вы уверены, что хотите заблокировать этого участника?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"В этой комнате нет заблокированных пользователей."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Блокировка %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d пользователь"</item>
|
||||
<item quantity="few">"%1$d пользователя"</item>
|
||||
<item quantity="many">"%1$d пользователей"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Удалить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Удалить участника из комнаты"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Удалить и запретить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Только удалить участника"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Удалить участника и запретить присоединяться в будущем?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Разблокировать"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Посмотреть информацию о пользователе"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"Они снова смогут присоединиться в эту комнату если их пригласят."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Разбанить пользователя?"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Посмотреть профиль"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Заблокирован"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Участники"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"В ожидании"</string>
|
||||
|
|
@ -49,6 +81,7 @@
|
|||
<string name="screen_room_member_list_role_administrator">"Администратор"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Модератор"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Участники комнаты"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Разблокировка %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Разрешить пользовательские настройки"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Включение этого параметра отменяет настройки по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Уведомить меня в этом чате"</string>
|
||||
|
|
@ -63,5 +96,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Администраторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Измените мою роль"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Понижение до участника"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Понизить до модератора"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Модерация участников"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Сообщения и содержание"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Модераторы"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Сбросить разрешения"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Как только вы сбросите разрешения, вы потеряете текущие настройки."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Сбросить разрешения?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Роли"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Информация о комнате"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Роли и разрешения"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."</string>
|
||||
<string name="screen_polls_history_title">"Ankety"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Iba správcovia"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Zakázať ľudí"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Odstrániť správy"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Všetci"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Pozvať ľudí"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Moderovanie členov"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Správy a obsah"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Správcovia a moderátori"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Odstrániť ľudí"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Zmeniť obrázok miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Podrobnosti o miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Zmeniť názov miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Zmeniť tému miestnosti"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Odoslať správy"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Upraviť správcov"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Túto akciu nebudete môcť vrátiť späť. Zvyšujete úroveň používateľa na rovnakú úroveň výkonu ako máte vy."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Pridať správcu?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Znížiť"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Túto zmenu nebudete môcť vrátiť späť, pretože znižujete svoju úroveň. Ak ste posledným privilegovaným používateľom v miestnosti, nebude možné získať znova oprávnenia."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Znížiť svoju úroveň?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Upraviť moderátorov"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené zmeny."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Uložiť zmeny?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Pridať tému"</string>
|
||||
<string name="screen_room_details_already_a_member">"Už ste členom"</string>
|
||||
<string name="screen_room_details_already_invited">"Už ste pozvaní"</string>
|
||||
|
|
@ -32,20 +55,25 @@
|
|||
<string name="screen_room_details_share_room_title">"Zdieľať miestnosť"</string>
|
||||
<string name="screen_room_details_topic_title">"Téma"</string>
|
||||
<string name="screen_room_details_updating_room">"Aktualizácia miestnosti…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Zakázať"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Ste si istý, že chcete zakázať tohto člena?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"V tejto miestnosti nie sú žiadni zakázaní používatelia."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Zakazuje sa %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d osoba"</item>
|
||||
<item quantity="few">"%1$d osoby"</item>
|
||||
<item quantity="other">"%1$d osôb"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odstrániť člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Odstrániť a zakázať člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Odstrániť z miestnosti"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Odstrániť a zakázať člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Iba odstrániť člena"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Odstrániť člena a zakázať vstup v budúcnosti?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Zrušiť zákaz"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Zrušiť zákaz používateľa"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Pozrieť informácie o používateľovi"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"Zobraziť profil"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Zakázaní"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Členovia"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Čaká sa"</string>
|
||||
|
|
@ -68,5 +96,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"V tejto miestnosti ma upozorniť na"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Správcovia"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Zmeniť moje oprávnenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Znížiť úroveň na člena"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Znížiť úroveň na moderátora"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Moderovanie členov"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Správy a obsah"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderátori"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Oprávnenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Obnoviť povolenia"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnení prídete o aktuálne nastavenia."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnoviť oprávnenia?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Roly"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti o miestnosti"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Roly a povolenia"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="screen_dm_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Avblockera användare"</string>
|
||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ett fel uppstod vid uppdatering av aviseringsinställningen."</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Alla"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Lägg till ämne"</string>
|
||||
<string name="screen_room_details_already_a_member">"Redan medlem"</string>
|
||||
<string name="screen_room_details_already_invited">"Redan inbjuden"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,27 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Під час оновлення налаштувань сповіщень сталася помилка."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."</string>
|
||||
<string name="screen_polls_history_title">"Опитування"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Тільки для адміністраторів"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Заблоковувати людей"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Видалити повідомлення"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Усі"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Запросити людей"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Модерація учасників"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Повідомлення та зміст"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Адміністратори та модератори"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Видалити людей"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Змінити аватар кімнати"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Деталі кімнати"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Змінити назву кімнати"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Змінити тему кімнати"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Надіслати повідомлення"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Керувати адмінами"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Ви не зможете скасувати цю дію. Ви просуваєте користувача, щоб він мав такий же рівень прав, як і ви."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Додати адміністратора?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Понизити"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Ви не зможете скасувати цю зміну, оскільки ви знижуєте себе, якщо ви останній привілейований користувач у кімнаті, відновити привілеї буде неможливо."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Понизити себе?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Керувати модераторами"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Додати тему"</string>
|
||||
<string name="screen_room_details_already_a_member">"Уже учасник"</string>
|
||||
<string name="screen_room_details_already_invited">"Уже запрошені"</string>
|
||||
|
|
@ -32,6 +53,9 @@
|
|||
<string name="screen_room_details_share_room_title">"Поділитися кімнатою"</string>
|
||||
<string name="screen_room_details_topic_title">"Тема"</string>
|
||||
<string name="screen_room_details_updating_room">"Оновлення кімнати…"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Заблокувати"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Він не зможе приєднатися до цієї кімнати знову, якщо його запросять."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Ви точно хочете заблокувати цього користувача?"</string>
|
||||
<string name="screen_room_member_list_banning_user">"Блокування %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d особа"</item>
|
||||
|
|
@ -68,5 +92,14 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"Всі повідомлення"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Тільки згадки та ключові слова"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"У цій кімнаті повідомляти мене про"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Адміністратори"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Модерація учасників"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Повідомлення та зміст"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Модератори"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Дозволи"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Скинути дозволи"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Ролі"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Деталі кімнати"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Ролі та дозволи"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
|
||||
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
|
||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"更新通知設定時發生錯誤。"</string>
|
||||
<string name="screen_polls_history_title">"所有投票"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"所有人"</string>
|
||||
<string name="screen_room_details_add_topic_title">"新增主題"</string>
|
||||
<string name="screen_room_details_already_a_member">"已是成員"</string>
|
||||
<string name="screen_room_details_already_invited">"已邀請"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@
|
|||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."</string>
|
||||
<string name="screen_polls_history_title">"Polls"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Admins only"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Ban people"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Delete messages"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Everyone"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Invite people"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Member moderation"</string>
|
||||
<string name="screen_room_change_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_change_permissions_moderators">"Admins and moderators"</string>
|
||||
<string name="screen_room_change_permissions_remove_people">"Remove people"</string>
|
||||
<string name="screen_room_change_permissions_room_avatar">"Change room avatar"</string>
|
||||
<string name="screen_room_change_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_change_permissions_room_name">"Change room name"</string>
|
||||
<string name="screen_room_change_permissions_room_topic">"Change room topic"</string>
|
||||
<string name="screen_room_change_permissions_send_messages">"Send messages"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Edit Admins"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"You will not be able to undo this action. You are promoting the user to have the same power level as you."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Add Admin?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Demote"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Demote yourself?"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Edit Moderators"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Add topic"</string>
|
||||
<string name="screen_room_details_already_a_member">"Already a member"</string>
|
||||
<string name="screen_room_details_already_invited">"Already invited"</string>
|
||||
|
|
@ -35,19 +58,21 @@
|
|||
<string name="screen_room_member_list_ban_member_confirmation_action">"Ban"</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_description">"They won’t be able to join this room again if invited."</string>
|
||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Are you sure you want to ban this member?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"There are no banned users in this room."</string>
|
||||
<string name="screen_room_member_list_banning_user">"Banning %1$s"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Remove member"</string>
|
||||
<string name="screen_room_member_list_manage_member_ban">"Remove and ban member"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove">"Remove from room"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Remove and ban member"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Only remove member"</string>
|
||||
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Remove member and ban from joining in the future?"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_action">"Unban"</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_message">"They will be able to join this room again if invited."</string>
|
||||
<string name="screen_room_member_list_manage_member_unban_title">"Unban user"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"See user info"</string>
|
||||
<string name="screen_room_member_list_manage_member_user_info">"View profile"</string>
|
||||
<string name="screen_room_member_list_mode_banned">"Banned"</string>
|
||||
<string name="screen_room_member_list_mode_members">"Members"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
||||
|
|
@ -70,5 +95,19 @@
|
|||
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"In this room, notify me for"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Admins"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Change my role"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Demote to member"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Demote to moderator"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Member moderation"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Messages and content"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Moderators"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Reset permissions"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Once you reset permissions, you will lose the current settings."</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Reset permissions?"</string>
|
||||
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
|
||||
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
|
||||
<string name="screen_room_roles_and_permissions_title">"Roles and permissions"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
invitePeople: () -> Unit = EnsureNeverCalled(),
|
||||
openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
openPollHistory: () -> Unit = EnsureNeverCalled(),
|
||||
openAdminSettings: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDetailsView(
|
||||
|
|
@ -270,6 +271,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
invitePeople = invitePeople,
|
||||
openAvatarPreview = openAvatarPreview,
|
||||
openPollHistory = openPollHistory,
|
||||
openAdminSettings = openAdminSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,21 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNavigator
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||
import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.featureflag.test.FakeFeatureFlagService
|
||||
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.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
|
|
@ -167,6 +175,51 @@ class RoomMemberListPresenterTests {
|
|||
assertThat(loadedState.canInvite).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest {
|
||||
val navigator = FakeRoomMemberListNavigator()
|
||||
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false)
|
||||
val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
|
||||
assertThat(navigator.openRoomMemberDetailsCallCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest {
|
||||
val navigator = FakeRoomMemberListNavigator()
|
||||
var selectRoomMemberCallCounts = 0
|
||||
val capturingState = aRoomMembersModerationState(eventSink = { event ->
|
||||
if (event is RoomMembersModerationEvents.SelectRoomMember) {
|
||||
selectRoomMemberCallCounts++
|
||||
}
|
||||
})
|
||||
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply {
|
||||
givenState(capturingState)
|
||||
}
|
||||
val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
|
||||
assertThat(selectRoomMemberCallCounts).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeRoomMemberListNavigator : RoomMemberListNavigator {
|
||||
var openRoomMemberDetailsCallCount = 0
|
||||
private set
|
||||
|
||||
override fun openRoomMemberDetails(userId: UserId) {
|
||||
openRoomMemberDetailsCallCount++
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
|
|
@ -182,4 +235,14 @@ private fun TestScope.createPresenter(
|
|||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
|
||||
) = RoomMemberListPresenter(matrixRoom, roomMemberListDataSource, coroutineDispatchers)
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)),
|
||||
moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(),
|
||||
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator { }
|
||||
) = RoomMemberListPresenter(
|
||||
room = matrixRoom,
|
||||
roomMemberListDataSource = roomMemberListDataSource,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
featureFlagService = featureFlagService,
|
||||
roomMembersModerationPresenter = moderationPresenter,
|
||||
navigator = navigator
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
* 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.members.moderation
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.DefaultRoomMembersModerationPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultRoomMembersModerationPresenterTests {
|
||||
@Test
|
||||
fun `canDisplayModerationActions - when feature flag is disabled returns false`() = runTest {
|
||||
val featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to false))
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(featureFlagService = featureFlagService)
|
||||
assertThat(presenter.canDisplayModerationActions()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canDisplayModerationActions - when room is DM is false`() = runTest {
|
||||
val room = FakeMatrixRoom(isDirect = true, isPublic = true, isOneToOne = true).apply {
|
||||
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
assertThat(presenter.canDisplayModerationActions()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest {
|
||||
val room = FakeMatrixRoom(isDirect = false, isOneToOne = false).apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
assertThat(presenter.canDisplayModerationActions()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest {
|
||||
val room = FakeMatrixRoom(isDirect = false, isOneToOne = false).apply {
|
||||
givenCanBanResult(Result.success(true))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
assertThat(presenter.canDisplayModerationActions()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aVictor()
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
with(awaitItem()) {
|
||||
assertThat(this.selectedRoomMember).isNotNull()
|
||||
assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId)
|
||||
assertThat(actions).containsExactly(
|
||||
ModerationAction.DisplayProfile(selectedMember.userId),
|
||||
ModerationAction.KickUser(selectedMember.userId),
|
||||
ModerationAction.BanUser(selectedMember.userId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest {
|
||||
val room = FakeMatrixRoom(sessionId = A_USER_ID).apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L)
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
with(awaitItem()) {
|
||||
assertThat(this.selectedRoomMember).isNotNull()
|
||||
assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId)
|
||||
assertThat(actions).containsExactly(
|
||||
ModerationAction.DisplayProfile(selectedMember.userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest {
|
||||
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedRoomMember).isNotNull()
|
||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Confirming)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Kick removes the user`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aVictor()
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().actions).isEmpty()
|
||||
assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||
with(awaitItem()) {
|
||||
assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BanUser requires confirmation and then bans the user`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val selectedMember = aVictor()
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.Confirming)
|
||||
|
||||
// Confirm
|
||||
confirmingState.eventSink(RoomMembersModerationEvents.BanUser)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().actions).isEmpty()
|
||||
assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||
with(awaitItem()) {
|
||||
assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest {
|
||||
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember)))
|
||||
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
// Displays confirmation dialog
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
||||
// Confirms unban
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.UnbanUser)
|
||||
assertThat(awaitItem().actions).isEmpty()
|
||||
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
||||
with(awaitItem()) {
|
||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(selectedRoomMember).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Reset removes the selected user and actions`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
// Displays confirmation dialog
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||
// Reset state
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.Reset)
|
||||
assertThat(awaitItem().selectedRoomMember).isNull()
|
||||
assertThat(awaitItem().actions).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Reset resets any async actions`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenCanKickResult(Result.success(true))
|
||||
givenCanBanResult(Result.success(true))
|
||||
givenKickUserResult(Result.failure(Throwable("Eek")))
|
||||
givenBanUserResult(Result.failure(Throwable("Eek")))
|
||||
givenUnbanUserResult(Result.failure(Throwable("Eek")))
|
||||
}
|
||||
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialItem = awaitItem()
|
||||
// Kick user and fail
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
||||
skipItems(2)
|
||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
// Reset it
|
||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
||||
assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
// Ban user and fail
|
||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
||||
skipItems(2)
|
||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
// Reset it
|
||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
||||
assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
// Unban user and fail
|
||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN)))
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
// Reset it
|
||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
||||
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultRoomMembersModerationPresenter(
|
||||
matrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
): DefaultRoomMembersModerationPresenter {
|
||||
return DefaultRoomMembersModerationPresenter(
|
||||
room = matrixRoom,
|
||||
featureFlagService = featureFlagService,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.members.moderation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
|
||||
class FakeRoomMembersModerationPresenter(
|
||||
private val canDisplayModerationActions: Boolean = true,
|
||||
) : RoomMembersModerationPresenter {
|
||||
private var state by mutableStateOf(dummyState())
|
||||
|
||||
override suspend fun canDisplayModerationActions(): Boolean {
|
||||
return canDisplayModerationActions
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMembersModerationState {
|
||||
return state
|
||||
}
|
||||
|
||||
fun givenState(state: RoomMembersModerationState) {
|
||||
this.state = state
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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.members.moderation
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||
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.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomMembersModerationViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Ignore("This test is not passing yet, need to investigate")
|
||||
@Test
|
||||
fun `clicking on back emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(roomMember.userId),
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
rule.pressBackKey()
|
||||
// Give time for the bottom sheet to animate
|
||||
rule.mainClock.advanceTimeBy(1_000)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on 'See user info' invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>(expectEvents = false)
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(roomMember.userId),
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnceWithParam(roomMember.userId) { callback ->
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
onDisplayMemberProfile = callback
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_member_list_manage_member_user_info)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on 'Remove member' emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(roomMember.userId),
|
||||
ModerationAction.KickUser(roomMember.userId),
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_member_list_manage_member_remove)
|
||||
// Give time for the bottom sheet to animate
|
||||
rule.mainClock.advanceTimeBy(1_000)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on 'Remove and ban member' emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(roomMember.userId),
|
||||
ModerationAction.KickUser(roomMember.userId),
|
||||
ModerationAction.BanUser(roomMember.userId),
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
// Note: the string key semantics is not perfect here :/
|
||||
rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)
|
||||
// Give time for the bottom sheet to animate
|
||||
rule.mainClock.advanceTimeBy(1_000)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancelling 'Remove and ban member' confirmation emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
banUserAsyncAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
// Note: the string key semantics is not perfect here :/
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirming 'Remove and ban member' confirmation emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
banUserAsyncAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
// Note: the string key semantics is not perfect here :/
|
||||
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancelling 'Unban member' confirmation emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
unbanUserAsyncAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
// Note: the string key semantics is not perfect here :/
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirming 'Unban member' confirmation emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
val roomMember = anAlice()
|
||||
val state = aRoomMembersModerationState(
|
||||
selectedRoomMember = roomMember,
|
||||
unbanUserAsyncAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setRoomMembersModerationView(
|
||||
state = state,
|
||||
)
|
||||
// Note: the string key semantics is not perfect here :/
|
||||
rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action)
|
||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomMembersModerationView(
|
||||
state: RoomMembersModerationState,
|
||||
onDisplayMemberProfile: (UserId) -> Unit = EnsureNeverCalledWithParam()
|
||||
) {
|
||||
setContent {
|
||||
RoomMembersModerationView(
|
||||
state = state,
|
||||
onDisplayMemberProfile = onDisplayMemberProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsPresenter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RolesAndPermissionPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createRolesAndPermissionsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(adminCount).isEqualTo(0)
|
||||
assertThat(moderatorCount).isEqualTo(0)
|
||||
assertThat(changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ChangeOwnRole presents a confirmation dialog`() = runTest {
|
||||
val presenter = createRolesAndPermissionsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole)
|
||||
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Confirming)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - DemoteSelfTo changes own role to the specified one`() = runTest(StandardTestDispatcher()) {
|
||||
val presenter = createRolesAndPermissionsPresenter(dispatchers = testCoroutineDispatchers())
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading)
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - DemoteSelfTo can handle failures and clean them`() = runTest(StandardTestDispatcher()) {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenUpdateUserRoleResult(Result.failure(Exception("Failed to update role")))
|
||||
}
|
||||
val presenter = createRolesAndPermissionsPresenter(room = room, dispatchers = testCoroutineDispatchers())
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR))
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading)
|
||||
|
||||
runCurrent()
|
||||
assertThat(awaitItem().changeOwnRoleAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
|
||||
initialState.eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelPendingAction dismisses confirmation dialog too`() = runTest {
|
||||
val presenter = createRolesAndPermissionsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole)
|
||||
awaitItem().eventSink(RolesAndPermissionsEvents.CancelPendingAction)
|
||||
|
||||
assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRolesAndPermissionsPresenter(
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
): RolesAndPermissionsPresenter {
|
||||
return RolesAndPermissionsPresenter(room = room, dispatchers = dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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.rolesandpermissions
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsState
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsView
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RoomDetailsAdminSettingsNavigator
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.aRolesAndPermissionsState
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RolesAndPermissionsViewTests {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `click on back invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRolesAndPermissionsView(
|
||||
goBack = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tapping on Admins opens admin list`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRolesAndPermissionsView(
|
||||
openAdminList = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_admins)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tapping on Moderators opens moderators list`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRolesAndPermissionsView(
|
||||
openModeratorList = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_roles_and_permissions_moderators)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRolesAndPermissionsView(
|
||||
state: RolesAndPermissionsState = aRolesAndPermissionsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
openAdminList: () -> Unit = EnsureNeverCalled(),
|
||||
openModeratorList: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
RolesAndPermissionsView(
|
||||
state = state,
|
||||
roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {
|
||||
override fun onBackPressed() = goBack()
|
||||
override fun openAdminList() = openAdminList()
|
||||
override fun openModeratorList() = openModeratorList()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* 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.rolesandpermissions.changeroles
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesPresenter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ChangeRolesPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createChangeRolesPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(role).isEqualTo(RoomMember.Role.ADMIN)
|
||||
assertThat(query).isNull()
|
||||
assertThat(isSearchActive).isFalse()
|
||||
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
assertThat(selectedUsers).isEmpty()
|
||||
assertThat(hasPendingChanges).isFalse()
|
||||
assertThat(exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial results are loaded automatically`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSearchActive changes the value`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.ToggleSearchActive)
|
||||
assertThat(awaitItem().isSearchActive).isTrue()
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.ToggleSearchActive)
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - QueryChanged produces new results`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty()
|
||||
assertThat(initialResults).hasSize(10)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.QueryChanged("Alice"))
|
||||
skipItems(1)
|
||||
|
||||
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty()
|
||||
assertThat(searchResults).hasSize(1)
|
||||
assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - changes in the room members produce new results`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty()
|
||||
assertThat(initialResults).hasSize(10)
|
||||
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList()))
|
||||
skipItems(1)
|
||||
|
||||
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty()
|
||||
assertThat(searchResults).hasSize(1)
|
||||
assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UserSelectionToggle adds and removes users from the selected user list`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
assertThat(awaitItem().selectedUsers).hasSize(2)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
assertThat(awaitItem().selectedUsers).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hasPendingChanges is true when the initial selected users don't match the new ones`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedUsers).hasSize(2)
|
||||
assertThat(hasPendingChanges).isTrue()
|
||||
}
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedUsers).hasSize(1)
|
||||
assertThat(hasPendingChanges).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Exit will display success if no pending changes`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelExit will remove exit confirmation`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Exit)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.exitState).isEqualTo(AsyncAction.Confirming)
|
||||
|
||||
confirmingState.eventSink(ChangeRolesEvent.CancelExit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Exit will display a confirmation dialog if there are pending changes, calling it again will actually exit`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.hasPendingChanges).isTrue()
|
||||
skipItems(1)
|
||||
|
||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Confirming)
|
||||
|
||||
updatedState.eventSink(ChangeRolesEvent.Exit)
|
||||
assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save will display a confirmation when adding admins`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming)
|
||||
|
||||
confirmingState.eventSink(ChangeRolesEvent.Save)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelSave will remove the confirmation dialog`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming)
|
||||
|
||||
confirmingState.eventSink(ChangeRolesEvent.CancelSave)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save will just save the data for moderators`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50)))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Save can handle failures and ClearError clears them`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50)))
|
||||
givenUpdateUserRoleResult(Result.failure(IllegalStateException("Failed")))
|
||||
}
|
||||
val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val failedState = awaitItem()
|
||||
assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
|
||||
failedState.eventSink(ChangeRolesEvent.ClearError)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createChangeRolesPresenter(
|
||||
role: RoomMember.Role = RoomMember.Role.ADMIN,
|
||||
room: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
): ChangeRolesPresenter {
|
||||
return ChangeRolesPresenter(
|
||||
role = role,
|
||||
room = room,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue