From 5e9496d98b88015e5f19d84547a30bde681fd28a Mon Sep 17 00:00:00 2001 From: enki Date: Sun, 26 Jul 2026 12:51:52 -0700 Subject: [PATCH] Rail click target fix + reorder mode via context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The rail's new name labels were dead zones (click lived only on the avatar), which shifted and broke taps across the whole rail — the entire item (avatar + label) is one click target now - Drag-to-arrange is an explicit mode entered from the room long-press menu ("Reorder rooms"): whole rows drag, a banner with Done exits. The previous avatar long-press handle fired the context menu at the same time --- .../home/impl/components/BlapSpaceRail.kt | 9 +++-- .../impl/components/RoomListContentView.kt | 37 ++++++++++++++++--- .../home/impl/roomlist/RoomListContextMenu.kt | 19 ++++++++++ .../home/impl/roomlist/RoomListEvent.kt | 3 ++ .../home/impl/roomlist/RoomListPresenter.kt | 8 ++++ .../home/impl/roomlist/RoomListState.kt | 2 + 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt index 7d8d87e812..33a08a25b9 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BlapSpaceRail.kt @@ -105,13 +105,16 @@ private fun RailItem( onClick: () -> Unit, content: @Composable () -> Unit, ) { + // The whole item (avatar + label) is one click target — a click only on the + // avatar left the label a dead zone and made the rail feel broken. Column( modifier = Modifier .fillMaxWidth() + .clickable(onClick = onClick) .padding(vertical = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - RailItemAvatar(isSelected = isSelected, onClick = onClick, content = content) + RailItemAvatar(isSelected = isSelected, content = content) Text( text = label, style = ElementTheme.typography.fontBodyXsRegular, @@ -129,7 +132,6 @@ private fun RailItem( @Composable private fun RailItemAvatar( isSelected: Boolean, - onClick: () -> Unit, content: @Composable () -> Unit, ) { Box( @@ -166,8 +168,7 @@ private fun RailItemAvatar( } else { Modifier } - ) - .clickable(onClick = onClick), + ), contentAlignment = Alignment.Center, ) { content() diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index c57a28e074..c80d67a708 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -15,8 +15,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -57,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Button 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.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -297,6 +300,29 @@ private fun RoomsViewList( } } + // Blap: drag-to-arrange banner while reorder mode is active. + if (state.blapReorderMode) { + item(key = "blap_reorder_banner", contentType = "blap_reorder_banner") { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(start = 16.dp, end = 8.dp), + ) { + Text( + text = "Drag rooms to arrange", + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textPrimary, + modifier = Modifier.weight(1f), + ) + TextButton( + text = stringResource(CommonStrings.action_done), + onClick = { eventSink(RoomListEvent.BlapSetReorderMode(false)) }, + ) + } + } + } // Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room // is moved to the top of the list. itemsIndexed( @@ -331,11 +357,11 @@ private fun RoomsViewList( ) { index, room -> ReorderableItem(blapReorderableState, key = room.roomId.value) { isDragging -> Column( - modifier = if (isDragging) { - Modifier.background(ElementTheme.colors.bgSubtleSecondary) - } else { - Modifier - }, + // In reorder mode the whole row is the drag handle (long-press + // drag conflicted with the row's context menu). + modifier = Modifier + .then(if (state.blapReorderMode) Modifier.draggableHandle() else Modifier) + .then(if (isDragging) Modifier.background(ElementTheme.colors.bgSubtleSecondary) else Modifier), ) { RoomSummaryRow( room = room, @@ -343,7 +369,6 @@ private fun RoomsViewList( isInviteSeen = false, showUnreadCount = state.showUnreadCount, onClick = onRoomClick, - blapAvatarModifier = Modifier.longPressDraggableHandle(), eventSink = eventSink, ) // Any room with an active call shows who's in it — not just classified diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt index 9d6a5173ae..8f63efe83d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt @@ -62,6 +62,10 @@ fun RoomListContextMenu( eventSink(RoomListEvent.HideContextMenu) onRoomSettingsClick(contextMenu.roomId) }, + onBlapReorderClick = { + eventSink(RoomListEvent.HideContextMenu) + eventSink(RoomListEvent.BlapSetReorderMode(true)) + }, onLeaveRoomClick = { eventSink(RoomListEvent.HideContextMenu) eventSink(RoomListEvent.LeaveRoom(contextMenu.roomId, needsConfirmation = true)) @@ -82,6 +86,7 @@ private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, canReportRoom: Boolean, onRoomSettingsClick: () -> Unit, + onBlapReorderClick: () -> Unit, onLeaveRoomClick: () -> Unit, onFavoriteChange: (isFavorite: Boolean) -> Unit, onRoomMarkReadClick: () -> Unit, @@ -129,6 +134,20 @@ private fun RoomListModalBottomSheetContent( ), ) } + // Blap: drag-to-arrange mode — long-press drag conflicted with this very menu, + // so reordering is an explicit mode instead. + ListItem( + headlineContent = { + Text( + text = "Reorder rooms", + style = MaterialTheme.typography.bodyLarge, + ) + }, + onClick = onBlapReorderClick, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.DragList()) + ), + ) val (textResId, icon) = if (contextMenu.isFavorite) { CommonStrings.common_favourited to CompoundIcons.FavouriteSolid() } else { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt index 0523449dba..a3f23b91fb 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt @@ -31,6 +31,9 @@ sealed interface RoomListEvent { data class BlapSetRoomOrder(val order: List) : RoomListEvent sealed interface ContextMenuEvent : RoomListEvent + + // Blap: enter/exit drag-to-arrange mode (entered from the room context menu) + data class BlapSetReorderMode(val enabled: Boolean) : ContextMenuEvent data object HideContextMenu : ContextMenuEvent data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvent data class MarkAsRead(val roomId: RoomId) : ContextMenuEvent diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index e432e27a96..f3686735e5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -159,6 +159,8 @@ class RoomListPresenter( ?.split(",")?.filter { it.isNotBlank() }?.map { RoomId(it) }.orEmpty() ) } + // Blap: drag-to-arrange mode, entered from the room context menu. + var blapReorderMode by remember { mutableStateOf(false) } // Blap: resolve profiles (display name + avatar) for users in active calls. var blapCallUsers by remember { mutableStateOf(emptyMap()) } val blapProfileAttempts = remember { mutableSetOf() } @@ -237,6 +239,9 @@ class RoomListPresenter( putString(blapOrderKey(selectedSpaceId), event.order.joinToString(",") { it.value }) } } + is RoomListEvent.BlapSetReorderMode -> { + blapReorderMode = event.enabled + } } } @@ -264,6 +269,7 @@ class RoomListPresenter( blapCollapsedSections, blapManualOrder, blapCallUsers, + blapReorderMode, ) return RoomListState( @@ -333,6 +339,7 @@ class RoomListPresenter( blapCollapsedSections: Set, blapManualOrder: List, blapCallUsers: Map, + blapReorderMode: Boolean, ): RoomListContentState { val roomSummaries by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) } @@ -370,6 +377,7 @@ class RoomListPresenter( blapCollapsedSections = blapCollapsedSections.toImmutableSet(), blapManualOrder = blapManualOrder.toImmutableList(), blapCallUsers = blapCallUsers.toImmutableMap(), + blapReorderMode = blapReorderMode, ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index 0f52c357f8..5939a55f83 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -86,5 +86,7 @@ sealed interface RoomListContentState { // profiles for users in active calls val blapManualOrder: ImmutableList = persistentListOf(), val blapCallUsers: ImmutableMap = persistentMapOf(), + // Blap: drag-to-arrange mode (whole rows become drag handles, Done exits) + val blapReorderMode: Boolean = false, ) : RoomListContentState }