Rail click target fix + reorder mode via context menu
Sonar / Sonar Quality Checks (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Post-release / post-release (push) Has been cancelled

- 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
This commit is contained in:
2026-07-26 12:51:52 -07:00
parent 762d6419ce
commit 5e9496d98b
6 changed files with 68 additions and 10 deletions
@@ -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()
@@ -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
@@ -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 {
@@ -31,6 +31,9 @@ sealed interface RoomListEvent {
data class BlapSetRoomOrder(val order: List<RoomId>) : 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
@@ -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<UserId, MatrixUser>()) }
val blapProfileAttempts = remember { mutableSetOf<UserId>() }
@@ -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<String>,
blapManualOrder: List<RoomId>,
blapCallUsers: Map<UserId, MatrixUser>,
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,
)
}
}
@@ -86,5 +86,7 @@ sealed interface RoomListContentState {
// profiles for users in active calls
val blapManualOrder: ImmutableList<RoomId> = persistentListOf(),
val blapCallUsers: ImmutableMap<UserId, MatrixUser> = persistentMapOf(),
// Blap: drag-to-arrange mode (whole rows become drag handles, Done exits)
val blapReorderMode: Boolean = false,
) : RoomListContentState
}