Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.features.rolesandpermissions.impl.RoomMemberListDataSource
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.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.annotations.RoomCoroutineScope
Expand Down Expand Up @@ -193,37 +194,27 @@ class ChangeRolesPresenter(
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
saveState: MutableState<AsyncAction<Boolean>>,
) = launch {
saveState.value = AsyncAction.Loading

val toAdd = selectedUsers.value - usersWithRole
val toRemove = usersWithRole - selectedUsers.value

val changes: List<UserRoleChange> = buildList {
for (selectedUser in toAdd) {
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, role.toAnalyticsMemberRole()))
add(UserRoleChange(selectedUser.userId, role))
}
for (selectedUser in toRemove) {
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
add(UserRoleChange(selectedUser.userId, RoomMember.Role.User))
runUpdatingState(saveState) {
val toAdd = selectedUsers.value - usersWithRole
val toRemove = usersWithRole - selectedUsers.value
val changes: List<UserRoleChange> = buildList {
for (selectedUser in toAdd) {
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, role.toAnalyticsMemberRole()))
add(UserRoleChange(selectedUser.userId, role))
}
for (selectedUser in toRemove) {
analyticsService.capture(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.User))
add(UserRoleChange(selectedUser.userId, RoomMember.Role.User))
}
}
room.updateUsersRoles(changes).map { true }
}

room.updateUsersRoles(changes)
.onFailure {
saveState.value = AsyncAction.Failure(it)
}
.onSuccess {
// Asynchronously reload the room members
launch { room.updateMembers() }
saveState.value = AsyncAction.Success(true)
}
}
}

internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
RoomMember.Role.User -> RoomModeration.Role.User
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
RoomMember.Role.User -> RoomModeration.Role.User
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncData
Expand All @@ -40,11 +39,8 @@ import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext

Expand All @@ -56,9 +52,10 @@ class RoomMemberListPresenter(
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
private val encryptionService: EncryptionService,
) : Presenter<RoomMemberListState> {
var roomMembers: AsyncData<RoomMembers> by mutableStateOf(AsyncData.Loading())

@Composable
override fun present(): RoomMemberListState {
var roomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var searchResults by remember {
mutableStateOf<SearchBarResultState<AsyncData<RoomMembers>>>(SearchBarResultState.Initial())
Expand All @@ -78,13 +75,9 @@ class RoomMemberListPresenter(
.launchIn(this)
}

// Update the room members when the screen is loaded or the active member count changes
// Update the room members when the screen is loaded
LaunchedEffect(Unit) {
room.roomInfoFlow.map { it.activeMembersCount }
.distinctUntilChanged()
.collectLatest {
room.updateMembers()
}
room.updateMembers()
}

LaunchedEffect(membersState, roomMemberIdentityStates) {
Expand Down Expand Up @@ -165,11 +158,7 @@ class RoomMemberListPresenter(
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
is RoomMemberListEvents.RoomMemberSelected ->
if (event.roomMember.membership == RoomMembershipState.BAN) {
roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser()))
} else {
roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser()))
}
roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser()))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
sealed interface InternalRoomMemberModerationEvents : RoomMemberModerationEvents {
data class DoKickUser(val reason: String) : InternalRoomMemberModerationEvents
data class DoBanUser(val reason: String) : InternalRoomMemberModerationEvents
data object DoUnbanUser : InternalRoomMemberModerationEvents
data class DoUnbanUser(val reason: String) : InternalRoomMemberModerationEvents
data object Reset : InternalRoomMemberModerationEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class RoomMemberModerationPresenter(
}
is InternalRoomMemberModerationEvents.DoUnbanUser -> {
selectedUser?.let {
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
coroutineScope.unbanUser(it.userId, event.reason, unbanUserAsyncAction)
}
selectedUser = null
}
Expand Down Expand Up @@ -198,10 +198,14 @@ class RoomMemberModerationPresenter(

private fun CoroutineScope.unbanUser(
userId: UserId,
reason: String,
unbanUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(unbanUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
room.unbanUser(userId = userId)
room.unbanUser(
userId = userId,
reason = reason.takeIf { it.isNotBlank() },
)
}

private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import io.element.android.libraries.designsystem.components.async.rememberAsyncI
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
Expand Down Expand Up @@ -93,12 +92,13 @@ private fun RoomMemberAsyncActions(
TextFieldDialog(
title = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_title),
submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action),
destructiveSubmit = true,
minLines = 2,
onSubmit = { reason ->
state.eventSink(InternalRoomMemberModerationEvents.DoKickUser(reason = reason))
},
onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
content = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_description),
value = "",
)
Expand Down Expand Up @@ -132,12 +132,13 @@ private fun RoomMemberAsyncActions(
TextFieldDialog(
title = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_title),
submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action),
destructiveSubmit = true,
minLines = 2,
onSubmit = { reason ->
state.eventSink(InternalRoomMemberModerationEvents.DoBanUser(reason = reason))
},
onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
content = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_description),
value = "",
)
Expand Down Expand Up @@ -167,18 +168,22 @@ private fun RoomMemberAsyncActions(
}
when (val action = state.unbanUserAsyncAction) {
is AsyncAction.Confirming -> {
ConfirmationDialog(
TextFieldDialog(
title = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_title),
content = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_description),
submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_action),
onSubmitClick = {
destructiveSubmit = true,
minLines = 2,
onSubmit = { reason ->
val userDisplayName = selectedUser?.getBestName().orEmpty()
asyncIndicatorState.enqueue {
AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_unbanning_user, userDisplayName))
}
state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser)
state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser(reason = reason))
},
onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
placeholder = stringResource(id = CommonStrings.common_reason),
content = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_description),
value = "",
)
}
is AsyncAction.Failure -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class RoomMemberModerationPresenterTest {
)
)
skipItems(2)
initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser)
initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser("Reason"))
skipItems(1)
val loadingState = awaitState()
assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class RoomMemberModerationViewTest {
),
)
rule.pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser(""))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fun ListDialog(
submitText: String = stringResource(CommonStrings.action_ok),
enabled: Boolean = true,
applyPaddingToContents: Boolean = true,
destructiveSubmit: Boolean = false,
listItems: LazyListScope.() -> Unit,
) {
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
Expand All @@ -65,6 +66,7 @@ fun ListDialog(
enabled = enabled,
listItems = listItems,
applyPaddingToContents = applyPaddingToContents,
destructiveSubmit = destructiveSubmit,
)
}
}
Expand All @@ -79,6 +81,7 @@ private fun ListDialogContent(
title: String?,
enabled: Boolean,
applyPaddingToContents: Boolean,
destructiveSubmit: Boolean,
subtitle: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
Expand All @@ -90,6 +93,7 @@ private fun ListDialogContent(
onSubmitClick = onSubmitClick,
enabled = enabled,
applyPaddingToContents = applyPaddingToContents,
destructiveSubmit = destructiveSubmit,
) {
// No start padding if padding is already applied to the content
val horizontalPadding = if (applyPaddingToContents) 0.dp else 8.dp
Expand Down Expand Up @@ -120,6 +124,7 @@ internal fun ListDialogContentPreview() {
cancelText = "Cancel",
submitText = "Save",
enabled = true,
destructiveSubmit = false,
applyPaddingToContents = true,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ fun TextFieldDialog(
validation: (String?) -> Boolean = { true },
onValidationErrorMessage: String? = null,
autoSelectOnDisplay: Boolean = true,
maxLines: Int = 1,
minLines: Int = 1,
maxLines: Int = minLines,
content: String? = null,
label: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
submitText: String = stringResource(CommonStrings.action_ok),
destructiveSubmit: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
Expand All @@ -67,6 +69,7 @@ fun TextFieldDialog(
onDismissRequest = onDismissRequest,
enabled = canSubmit,
submitText = submitText,
destructiveSubmit = destructiveSubmit,
modifier = modifier,
) {
if (content != null) {
Expand All @@ -93,6 +96,7 @@ fun TextFieldDialog(
onSubmit(textFieldContents.text)
}
}),
minLines = minLines,
maxLines = maxLines,
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ fun TextFieldListItem(
onTextChange: (String) -> Unit,
modifier: Modifier = Modifier,
error: String? = null,
maxLines: Int = 1,
minLines: Int = 1,
maxLines: Int = minLines,
label: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
Expand All @@ -53,7 +54,8 @@ fun TextFieldListItem(
onTextChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
error: String? = null,
maxLines: Int = 1,
minLines: Int = 1,
maxLines: Int = minLines,
label: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
Expand All @@ -68,6 +70,7 @@ fun TextFieldListItem(
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
maxLines = maxLines,
minLines = minLines,
singleLine = maxLines == 1,
modifier = modifier,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface Timeline : AutoCloseable {

val mode: Mode
val membershipChangeEventReceived: Flow<Unit>
val onSyncedEventReceived: Flow<Unit>
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
Expand Down
Loading
Loading