feat(desktop): Messages draggable divider + alignment polish + centered empty states
- **Draggable list pane**: the 280dp-fixed conversation list width is now user-adjustable via a draggable divider between the list and the chat pane. Width is clamped to 220-480dp and persisted across app restarts via DesktopPreferences.messagesListWidthDp. Cursor flips to the horizontal-resize arrow on hover. - **ConversationCard alignment**: the unread indicator used to sit as an 8dp dot + 14dp spacer to the left of the avatar, which pushed every card ~14dp inward from the header row's 12dp padding. The avatar now starts flush with the header (aligned with the top-bar buttons); the unread state is drawn as a 10dp dot overlaid on the avatar's bottom-right corner (iMessage / Slack-style), with a surface-matched ring so it reads clearly against the avatar. - **Drafts empty state**: was a flush-left Text pinned 16dp below the header; now uses the shared EmptyState composable so "No drafts yet" / description is vertically + horizontally centered in the available space, consistent with Notifications / Highlights / etc. https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
@@ -37,6 +37,7 @@ object DesktopPreferences {
|
||||
private const val KEY_LAST_SCREEN = "last_screen"
|
||||
private const val KEY_DECK_COLUMNS = "deck_columns"
|
||||
private const val KEY_LAYOUT_MODE = "layout_mode"
|
||||
private const val KEY_MESSAGES_LIST_WIDTH = "messages_list_width_dp"
|
||||
|
||||
var feedMode: FeedMode
|
||||
get() {
|
||||
@@ -69,6 +70,13 @@ object DesktopPreferences {
|
||||
prefs.put(KEY_LAYOUT_MODE, value)
|
||||
}
|
||||
|
||||
/** Width (dp) of the conversation list pane in the Messages screen. */
|
||||
var messagesListWidthDp: Float
|
||||
get() = prefs.getFloat(KEY_MESSAGES_LIST_WIDTH, 280f)
|
||||
set(value) {
|
||||
prefs.putFloat(KEY_MESSAGES_LIST_WIDTH, value)
|
||||
}
|
||||
|
||||
private const val KEY_WORKSPACES = "workspaces"
|
||||
|
||||
var workspaces: String
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.desktop.service.drafts.DesktopDraftStore
|
||||
import com.vitorpamplona.amethyst.desktop.service.drafts.DraftEntry
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -94,13 +95,10 @@ fun DraftsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (drafts.isEmpty()) {
|
||||
Text(
|
||||
"No drafts yet. Click \"New Draft\" to start writing.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
EmptyState(
|
||||
title = "No drafts yet",
|
||||
description = "Click \"New Draft\" to start writing.",
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
|
||||
+32
-28
@@ -306,35 +306,39 @@ private fun ConversationCard(
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Unread indicator
|
||||
if (item.hasUnread) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
} else {
|
||||
Spacer(Modifier.width(14.dp))
|
||||
}
|
||||
|
||||
// Avatar
|
||||
// Avatar — starts flush with the header's 12dp padding. Unread state is
|
||||
// signalled by a small primary-coloured dot overlaid on the avatar's
|
||||
// bottom-right corner (Slack / iMessage-style), not an inline indent
|
||||
// that would push the whole card to the right of the header label.
|
||||
val firstUser = item.users.firstOrNull()
|
||||
if (firstUser != null) {
|
||||
UserAvatar(
|
||||
userHex = firstUser.pubkeyHex,
|
||||
pictureUrl = firstUser.profilePicture(),
|
||||
size = 40.dp,
|
||||
)
|
||||
} else if (item.isGroup) {
|
||||
Icon(
|
||||
MaterialSymbols.Group,
|
||||
contentDescription = "Group",
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Box {
|
||||
if (firstUser != null) {
|
||||
UserAvatar(
|
||||
userHex = firstUser.pubkeyHex,
|
||||
pictureUrl = firstUser.profilePicture(),
|
||||
size = 40.dp,
|
||||
)
|
||||
} else if (item.isGroup) {
|
||||
Icon(
|
||||
MaterialSymbols.Group,
|
||||
contentDescription = "Group",
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (item.hasUnread) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(1.5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(10.dp))
|
||||
|
||||
+48
-2
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.desktop.ui.chats
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
@@ -51,6 +52,9 @@ import androidx.compose.ui.input.key.isShiftPressed
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -63,6 +67,7 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.awt.Cursor
|
||||
|
||||
private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
||||
|
||||
@@ -273,6 +278,15 @@ private fun SplitMessagesContent(
|
||||
onShowRelayPicker: () -> Unit = {},
|
||||
keyHandler: Modifier,
|
||||
) {
|
||||
// Draggable split: the conversation list width is user-adjustable and
|
||||
// persisted so it survives app restarts.
|
||||
var listWidth by remember {
|
||||
androidx.compose.runtime.mutableStateOf(
|
||||
com.vitorpamplona.amethyst.desktop.DesktopPreferences.messagesListWidthDp.dp,
|
||||
)
|
||||
}
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().then(keyHandler)) {
|
||||
ConversationListPane(
|
||||
state = listState,
|
||||
@@ -281,10 +295,17 @@ private fun SplitMessagesContent(
|
||||
onNewConversation = onShowNewDm,
|
||||
onShowRelayPicker = onShowRelayPicker,
|
||||
focusRequester = listFocusRequester,
|
||||
modifier = Modifier.width(280.dp),
|
||||
modifier = Modifier.width(listWidth),
|
||||
)
|
||||
|
||||
VerticalDivider(modifier = Modifier.fillMaxHeight())
|
||||
MessagesDraggableDivider(
|
||||
onDrag = { deltaPx ->
|
||||
val deltaDp = with(density) { deltaPx.toDp() }
|
||||
val next = (listWidth + deltaDp).coerceIn(220.dp, 480.dp)
|
||||
listWidth = next
|
||||
com.vitorpamplona.amethyst.desktop.DesktopPreferences.messagesListWidthDp = next.value
|
||||
},
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
@@ -357,3 +378,28 @@ private fun EmptyConversationState() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draggable vertical divider between the conversation list pane and the chat
|
||||
* pane. 12dp wide hit area; cursor flips to the horizontal resize arrow on
|
||||
* hover.
|
||||
*/
|
||||
@Composable
|
||||
private fun MessagesDraggableDivider(onDrag: (Float) -> Unit) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.width(12.dp)
|
||||
.fillMaxHeight()
|
||||
.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures { change, dragAmount ->
|
||||
change.consume()
|
||||
onDrag(dragAmount.x)
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
VerticalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user