diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt index 3207543e0f6..2b02cdfdb29 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt @@ -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 @@ -193,37 +194,27 @@ class ChangeRolesPresenter( selectedUsers: MutableState>, saveState: MutableState>, ) = launch { - saveState.value = AsyncAction.Loading - - val toAdd = selectedUsers.value - usersWithRole - val toRemove = usersWithRole - selectedUsers.value - - val changes: List = 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 = 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 + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 234de736755..92705b17751 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -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 @@ -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 @@ -56,9 +52,10 @@ class RoomMemberListPresenter( private val roomMembersModerationPresenter: Presenter, private val encryptionService: EncryptionService, ) : Presenter { + var roomMembers: AsyncData by mutableStateOf(AsyncData.Loading()) + @Composable override fun present(): RoomMemberListState { - var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } var searchQuery by rememberSaveable { mutableStateOf("") } var searchResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) @@ -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) { @@ -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())) } } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt index 2679de4742f..2bc76db7619 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt @@ -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 } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 593ae2386b5..31ff2da2b27 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -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 } @@ -198,10 +198,14 @@ class RoomMemberModerationPresenter( private fun CoroutineScope.unbanUser( userId: UserId, + reason: String, unbanUserAction: MutableState>, ) = runActionAndWaitForMembershipChange(unbanUserAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId = userId) + room.unbanUser( + userId = userId, + reason = reason.takeIf { it.isNotBlank() }, + ) } private fun CoroutineScope.runActionAndWaitForMembershipChange( diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index 396f9bac10b..1e7251b20b1 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -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 @@ -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 = "", ) @@ -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 = "", ) @@ -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 -> { diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt index b4ab27cbf1c..2b3f71e7706 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -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) diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt index a6caa9f013d..6508b28053a 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -182,7 +182,7 @@ class RoomMemberModerationViewTest { ), ) rule.pressTag(TestTags.dialogPositive.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) } @Test diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt index e9a8e627212..ce1afae93d3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt @@ -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 { @@ -65,6 +66,7 @@ fun ListDialog( enabled = enabled, listItems = listItems, applyPaddingToContents = applyPaddingToContents, + destructiveSubmit = destructiveSubmit, ) } } @@ -79,6 +81,7 @@ private fun ListDialogContent( title: String?, enabled: Boolean, applyPaddingToContents: Boolean, + destructiveSubmit: Boolean, subtitle: @Composable (() -> Unit)? = null, ) { SimpleAlertDialogContent( @@ -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 @@ -120,6 +124,7 @@ internal fun ListDialogContentPreview() { cancelText = "Cancel", submitText = "Save", enabled = true, + destructiveSubmit = false, applyPaddingToContents = true, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt index cfc67988c85..aeaaa9f040f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt @@ -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) { @@ -67,6 +69,7 @@ fun TextFieldDialog( onDismissRequest = onDismissRequest, enabled = canSubmit, submitText = submitText, + destructiveSubmit = destructiveSubmit, modifier = modifier, ) { if (content != null) { @@ -93,6 +96,7 @@ fun TextFieldDialog( onSubmit(textFieldContents.text) } }), + minLines = minLines, maxLines = maxLines, modifier = Modifier .fillMaxWidth() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt index fc2c1c91cfd..e149af58909 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt @@ -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, @@ -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, @@ -68,6 +70,7 @@ fun TextFieldListItem( keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, maxLines = maxLines, + minLines = minLines, singleLine = maxLines == 1, modifier = modifier, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 5774869aa2d..500d9f31914 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -55,6 +55,7 @@ interface Timeline : AutoCloseable { val mode: Mode val membershipChangeEventReceived: Flow + val onSyncedEventReceived: Flow suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result suspend fun markAsRead(receiptType: ReceiptType): Result suspend fun paginate(direction: PaginationDirection): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index 1dd50af7f28..0148b45aa83 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop @@ -58,6 +59,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener @@ -92,8 +94,6 @@ class JoinedRustRoom( private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) private val innerRoom = baseRoom.innerRoom - override val syncUpdateFlow = MutableStateFlow(0L) - override val roomTypingMembersFlow: Flow> = mxCallbackFlow { val initial = emptyList() channel.trySend(initial) @@ -136,11 +136,21 @@ class JoinedRustRoom( override val roomNotificationSettingsStateFlow = MutableStateFlow(RoomNotificationSettingsState.Unknown) - override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) { - syncUpdateFlow.value = systemClock.epochMillis() - } + override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) + + override val syncUpdateFlow = liveTimeline.onSyncedEventReceived + .map { systemClock.epochMillis() } + .stateIn( + scope = roomCoroutineScope, + started = WhileSubscribed(), + initialValue = systemClock.epochMillis(), + ) init { + subscribeToRoomMembersChange() + } + + private fun subscribeToRoomMembersChange() { val powerLevelChanges = roomInfoFlow.map { it.roomPowerLevels }.distinctUntilChanged() val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) } combine(membershipChanges, powerLevelChanges) { _, _ -> } @@ -479,7 +489,6 @@ class JoinedRustRoom( private fun InnerTimeline.map( mode: Timeline.Mode, - onNewSyncedEvent: () -> Unit = {}, ): Timeline { val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$this") return RustTimeline( @@ -490,7 +499,6 @@ class JoinedRustRoom( coroutineScope = timelineCoroutineScope, dispatcher = roomDispatcher, roomContentForwarder = roomContentForwarder, - onNewSyncedEvent = onNewSyncedEvent, ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index f31c34c0b02..12a25c3169d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -8,9 +8,10 @@ package io.element.android.libraries.matrix.impl.timeline +import androidx.compose.ui.util.fastForEach import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent -import kotlinx.coroutines.flow.Flow +import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex @@ -21,58 +22,60 @@ import timber.log.Timber internal class MatrixTimelineDiffProcessor( private val timelineItems: MutableSharedFlow>, - private val timelineItemFactory: MatrixTimelineItemMapper, + private val membershipChangeEventReceivedFlow: MutableSharedFlow, + private val syncedEventReceivedFlow: MutableSharedFlow, + private val timelineItemMapper: MatrixTimelineItemMapper, ) { private val mutex = Mutex() - private val _membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) - val membershipChangeEventReceived: Flow = _membershipChangeEventReceived - suspend fun postDiffs(diffs: List) { - updateTimelineItems { + mutex.withLock { Timber.v("Update timeline items from postDiffs (with ${diffs.size} items) on ${Thread.currentThread()}") - diffs.forEach { diff -> - applyDiff(diff) + val result = processDiffs(diffs) + timelineItems.emit(result.items()) + if (result.hasNewEventsFromSync()) { + syncedEventReceivedFlow.emit(Unit) + } + if (result.hasMembershipChangeEventFromSync()) { + membershipChangeEventReceivedFlow.emit(Unit) } } } - private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = - mutex.withLock { - val mutableTimelineItems = if (timelineItems.replayCache.isNotEmpty()) { - timelineItems.first().toMutableList() - } else { - mutableListOf() - } - block(mutableTimelineItems) - timelineItems.tryEmit(mutableTimelineItems) + private suspend fun processDiffs(diffs: List): DiffingResult { + val mutableTimelineItems = if (timelineItems.replayCache.isNotEmpty()) { + timelineItems.first().toMutableList() + } else { + mutableListOf() } + val result = DiffingResult(items = mutableTimelineItems) + diffs.forEach { diff -> + result.applyDiff(diff) + } + return result + } - private fun MutableList.applyDiff(diff: TimelineDiff) { + private fun DiffingResult.applyDiff(diff: TimelineDiff) { when (diff) { is TimelineDiff.Append -> { - val items = diff.values.map { it.asMatrixTimelineItem() } - addAll(items) + diff.values.fastForEach { item -> + add(item.map()) + } } is TimelineDiff.PushBack -> { - val item = diff.value.asMatrixTimelineItem() - if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) { - // TODO - This is a temporary solution to notify the room screen about membership changes - // Ideally, this should be implemented by the Rust SDK - _membershipChangeEventReceived.tryEmit(Unit) - } + val item = diff.value.map() add(item) } is TimelineDiff.PushFront -> { - val item = diff.value.asMatrixTimelineItem() + val item = diff.value.map() add(0, item) } is TimelineDiff.Set -> { - val item = diff.value.asMatrixTimelineItem() + val item = diff.value.map() set(diff.index.toInt(), item) } is TimelineDiff.Insert -> { - val item = diff.value.asMatrixTimelineItem() + val item = diff.value.map() add(diff.index.toInt(), item) } is TimelineDiff.Remove -> { @@ -80,25 +83,91 @@ internal class MatrixTimelineDiffProcessor( } is TimelineDiff.Reset -> { clear() - val items = diff.values.map { it.asMatrixTimelineItem() } - addAll(items) + diff.values.fastForEach { item -> + add(item.map()) + } } TimelineDiff.PopFront -> { - removeFirstOrNull() + removeFirst() } TimelineDiff.PopBack -> { - removeLastOrNull() + removeLast() } TimelineDiff.Clear -> { clear() } is TimelineDiff.Truncate -> { - subList(diff.length.toInt(), size).clear() + truncate(diff.length.toInt()) + } + } + } + + private fun TimelineItem.map(): MatrixTimelineItem { + return timelineItemMapper.map(this) + } +} + +private class DiffingResult( + private val items: MutableList, + private var hasNewEventsFromSync: Boolean = false, + private var hasMembershipChangeEventFromSync: Boolean = false, +) { + fun items(): List = items + fun hasNewEventsFromSync(): Boolean = hasNewEventsFromSync + fun hasMembershipChangeEventFromSync(): Boolean = hasMembershipChangeEventFromSync + + fun add(item: MatrixTimelineItem) { + processItem(item) + items.add(item) + } + + fun add(index: Int, item: MatrixTimelineItem) { + processItem(item) + items.add(index, item) + } + + fun set(index: Int, item: MatrixTimelineItem) { + processItem(item) + items[index] = item + } + + fun removeAt(index: Int) { + items.removeAt(index) + } + + fun removeFirst() { + items.removeFirstOrNull() + } + + fun removeLast() { + items.removeLastOrNull() + } + + fun truncate(length: Int) { + items.subList(length, items.size).clear() + } + + fun clear() { + items.clear() + } + + private fun processItem(item: MatrixTimelineItem) { + if (skipProcessing()) return + when (item) { + is MatrixTimelineItem.Event -> { + if (item.event.origin == TimelineItemEventOrigin.SYNC) { + hasNewEventsFromSync = true + when (item.event.content) { + is RoomMembershipContent -> hasMembershipChangeEventFromSync = true + else -> Unit + } + } } + else -> Unit } } - private fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem { - return timelineItemFactory.map(this) + private fun skipProcessing(): Boolean { + return hasNewEventsFromSync && hasMembershipChangeEventFromSync } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index c2d26b19449..391349b52da 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -81,16 +81,18 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, override val mode: Timeline.Mode, - systemClock: SystemClock, + private val systemClock: SystemClock, private val joinedRoom: JoinedRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val roomContentForwarder: RoomContentForwarder, - onNewSyncedEvent: () -> Unit, ) : Timeline { private val _timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + private val _membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) + private val _onSyncedEventReceived: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + private val timelineEventContentMapper = TimelineEventContentMapper() private val inReplyToMapper = InReplyToMapper(timelineEventContentMapper) private val timelineItemMapper = MatrixTimelineItemMapper( @@ -99,18 +101,19 @@ class RustTimeline( virtualTimelineItemMapper = VirtualTimelineItemMapper(), eventTimelineItemMapper = EventTimelineItemMapper( contentMapper = timelineEventContentMapper - ) + ), ) private val timelineDiffProcessor = MatrixTimelineDiffProcessor( timelineItems = _timelineItems, - timelineItemFactory = timelineItemMapper, + membershipChangeEventReceivedFlow = _membershipChangeEventReceived, + syncedEventReceivedFlow = _onSyncedEventReceived, + timelineItemMapper = timelineItemMapper, ) private val timelineItemsSubscriber = TimelineItemsSubscriber( timeline = inner, timelineCoroutineScope = coroutineScope, timelineDiffProcessor = timelineDiffProcessor, dispatcher = dispatcher, - onNewSyncedEvent = onNewSyncedEvent, ) private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode) @@ -152,7 +155,13 @@ class RustTimeline( .launchIn(this) } - override val membershipChangeEventReceived: Flow = timelineDiffProcessor.membershipChangeEventReceived + override val membershipChangeEventReceived: Flow = _membershipChangeEventReceived + .onStart { timelineItemsSubscriber.subscribeIfNeeded() } + .onCompletion { timelineItemsSubscriber.unsubscribeIfNeeded() } + + override val onSyncedEventReceived: Flow = _onSyncedEventReceived + .onStart { timelineItemsSubscriber.subscribeIfNeeded() } + .onCompletion { timelineItemsSubscriber.unsubscribeIfNeeded() } override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result = withContext(dispatcher) { runCatchingExceptions { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineDiffExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineDiffExt.kt deleted file mode 100644 index 011ea7e21ce..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineDiffExt.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.timeline - -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineItem -import uniffi.matrix_sdk_ui.EventItemOrigin - -/** - * Tries to get an event origin from the TimelineDiff. - * If there is multiple events in the diff, uses the first one as it should be a good indicator. - */ -internal fun TimelineDiff.eventOrigin(): EventItemOrigin? { - return when (this) { - is TimelineDiff.Append -> values.firstOrNull()?.eventOrigin() - is TimelineDiff.PushBack -> value.eventOrigin() - is TimelineDiff.PushFront -> value.eventOrigin() - is TimelineDiff.Set -> value.eventOrigin() - is TimelineDiff.Insert -> value.eventOrigin() - is TimelineDiff.Reset -> values.firstOrNull()?.eventOrigin() - else -> null - } -} - -private fun TimelineItem.eventOrigin(): EventItemOrigin? { - return asEvent()?.origin -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt index 4c7cd37ff90..adf9102b3e3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt @@ -12,13 +12,11 @@ import io.element.android.libraries.core.coroutine.childScope import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.rustcomponents.sdk.Timeline -import uniffi.matrix_sdk_ui.EventItemOrigin /** * This class is responsible for subscribing to a timeline and post the items/diffs to the timelineDiffProcessor. @@ -29,7 +27,6 @@ internal class TimelineItemsSubscriber( dispatcher: CoroutineDispatcher, private val timeline: Timeline, private val timelineDiffProcessor: MatrixTimelineDiffProcessor, - private val onNewSyncedEvent: () -> Unit, ) { private var subscriptionCount = 0 private val mutex = Mutex() @@ -44,9 +41,6 @@ internal class TimelineItemsSubscriber( if (subscriptionCount == 0) { timeline.timelineDiffFlow() .onEach { diffs -> - if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) { - onNewSyncedEvent() - } timelineDiffProcessor.postDiffs(diffs) } .launchIn(coroutineScope) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt index fe923f6d835..ba7f640514b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt @@ -169,10 +169,12 @@ class MatrixTimelineDiffProcessorTest { } internal fun TestScope.createMatrixTimelineDiffProcessor( - timelineItems: MutableSharedFlow>, -): MatrixTimelineDiffProcessor { + timelineItems: MutableSharedFlow> = MutableSharedFlow(), + membershipChangeEventReceivedFlow: MutableSharedFlow = MutableSharedFlow(), + syncedEventReceivedFlow: MutableSharedFlow = MutableSharedFlow(), + ): MatrixTimelineDiffProcessor { val timelineEventContentMapper = TimelineEventContentMapper() - val timelineItemMapper = MatrixTimelineItemMapper( + val timelineItemFactory = MatrixTimelineItemMapper( fetchDetailsForEvent = { _ -> Result.success(Unit) }, coroutineScope = this, virtualTimelineItemMapper = VirtualTimelineItemMapper(), @@ -182,6 +184,8 @@ internal fun TestScope.createMatrixTimelineDiffProcessor( ) return MatrixTimelineDiffProcessor( timelineItems = timelineItems, - timelineItemFactory = timelineItemMapper, + membershipChangeEventReceivedFlow = membershipChangeEventReceivedFlow, + syncedEventReceivedFlow = syncedEventReceivedFlow, + timelineItemMapper = timelineItemFactory, ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt index def4fc7cd06..1dde04575eb 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt @@ -99,7 +99,6 @@ private fun TestScope.createRustTimeline( coroutineScope: CoroutineScope = backgroundScope, dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io, roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeFfiRoomListService()), - onNewSyncedEvent: () -> Unit = {}, ): RustTimeline { return RustTimeline( inner = inner, @@ -109,6 +108,5 @@ private fun TestScope.createRustTimeline( coroutineScope = coroutineScope, dispatcher = dispatcher, roomContentForwarder = roomContentForwarder, - onNewSyncedEvent = onNewSyncedEvent, ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt index b9a6d243f49..4accf26e68d 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt @@ -14,8 +14,6 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.impl.fixtures.factories.aRustEventTimelineItem import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem -import io.element.android.tests.testutils.lambda.lambdaError -import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.StandardTestDispatcher @@ -36,9 +34,12 @@ class TimelineItemsSubscriberTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) val timeline = FakeFfiTimeline() + val diffProcessor = createMatrixTimelineDiffProcessor( + timelineItems = timelineItems, + ) val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, - timelineItems = timelineItems, + timelineDiffProcessor = diffProcessor, ) timelineItems.test { timelineItemsSubscriber.subscribeIfNeeded() @@ -57,9 +58,12 @@ class TimelineItemsSubscriberTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) val timeline = FakeFfiTimeline() + val diffProcessor = createMatrixTimelineDiffProcessor( + timelineItems = timelineItems, + ) val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, - timelineItems = timelineItems, + timelineDiffProcessor = diffProcessor, ) timelineItems.test { timelineItemsSubscriber.subscribeIfNeeded() @@ -74,15 +78,16 @@ class TimelineItemsSubscriberTest { @Ignore("JNA direct mapping has broken unit tests with FFI fakes") @Test - fun `when timeline emits an item with SYNC origin, the callback onNewSyncedEvent is invoked`() = runTest { + fun `when timeline emits an item with SYNC origin`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) val timeline = FakeFfiTimeline() - val onNewSyncedEventRecorder = lambdaRecorder { } + val diffProcessor = createMatrixTimelineDiffProcessor( + timelineItems = timelineItems, + ) val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, - timelineItems = timelineItems, - onNewSyncedEvent = onNewSyncedEventRecorder, + timelineDiffProcessor = diffProcessor, ) timelineItems.test { timelineItemsSubscriber.subscribeIfNeeded() @@ -101,7 +106,6 @@ class TimelineItemsSubscriberTest { assertThat(final).isNotEmpty() timelineItemsSubscriber.unsubscribeIfNeeded() } - onNewSyncedEventRecorder.assertions().isCalledOnce() } @Ignore("JNA direct mapping has broken unit tests with FFI fakes") @@ -117,14 +121,12 @@ class TimelineItemsSubscriberTest { private fun TestScope.createTimelineItemsSubscriber( timeline: Timeline = FakeFfiTimeline(), - timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE), - onNewSyncedEvent: () -> Unit = { lambdaError() }, + timelineDiffProcessor: MatrixTimelineDiffProcessor = createMatrixTimelineDiffProcessor(), ): TimelineItemsSubscriber { return TimelineItemsSubscriber( timelineCoroutineScope = backgroundScope, dispatcher = StandardTestDispatcher(testScheduler), timeline = timeline, - timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems), - onNewSyncedEvent = onNewSyncedEvent, + timelineDiffProcessor = timelineDiffProcessor, ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 87fa098b1cc..6115dd2345d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -48,6 +48,7 @@ class FakeTimeline( ) ), override val membershipChangeEventReceived: Flow = MutableSharedFlow(), + override val onSyncedEventReceived: Flow = MutableSharedFlow(), private val cancelSendResult: (TransactionId) -> Result = { lambdaError() }, override val mode: Timeline.Mode = Timeline.Mode.Live, private val markAsReadResult: (ReceiptType) -> Result = { lambdaError() }, diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png index edec0550cad..e8798e5d639 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4063555b9c55be91104f51d5696383aba383d2b7b8d6ff490058f0228e45f81 -size 30659 +oid sha256:e8f2b0a758dd5aa20f5e54a1c2e5b094dae58cecbd51b14e6badee0de0d4f47c +size 29661 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png index fe4528f99c3..2fbf80638e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72ccd513cba258987e43054b269006625b76510efc26e4bc62ec2801f77be662 -size 28093 +oid sha256:3ca2cc0fb3cc31ee9d15b5c7d3c723b94fd4877bd25d419808d7c48f8e358496 +size 26527 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png index 9c6513bfd19..4dd6dfae72d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a7da5666d690d5d9a6be61225b169e4703626ebc15f2f8f4f3e98d2e322de39 -size 28831 +oid sha256:2708604aa1f4d6549b80ff388c45449c73577f39903810c438361fc068e440f9 +size 27813 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png index 1bef3fc7be5..4f696f4f067 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28f71f91a53008d73ed1f7cf209c7732b6fe0f4d7f65c2cf3997dff99f27c5e4 -size 26402 +oid sha256:7aa3113919aad2ea9cb7fd6b5b4aee6c9aa1ab600ee6cd6d18f75cc70469d1cb +size 25084