mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-11 17:35:01 +00:00
desktop: keep text selection on the originally selected message when a new message arrives (#6955)
* desktop: keep text selection on the originally selected message when a new message arrives or is sent Selection stored positional indices into the merged-items list. When a new message was appended, the reversed list shifted every index by one — but the stored start/end indices did not — so the highlight slid onto neighboring messages. Anchor the selection to ChatItem IDs instead of list positions. Offsets are already content-relative (character cursors in rendered text), so they stay valid across list mutations. Positional indices are derived on read via derivedStateOf, which keeps per-item reads O(1) amortized. If either anchored item is removed from the list, a SideEffect synchronously clears the selection so the copy button does not flash at a stale location. * desktop: minimize selection fix — anchor ids in SelectionRange Replaces the previous derivedStateOf-based approach with a surgical diff: SelectionRange carries startItemId/endItemId alongside the existing positional indices, and a SideEffect calls resyncIndices() to translate ids back to current positions when the items list mutates. All existing call sites of range.startIndex / range.endIndex remain unchanged. Net diff vs master is +19/-2. * plans: justify desktop text selection id-anchored fix
This commit is contained in:
+20
-2
@@ -53,8 +53,10 @@ val LocalItemContext = compositionLocalOf { ItemContext() }
|
||||
|
||||
data class SelectionRange(
|
||||
val startIndex: Int,
|
||||
val startItemId: Long,
|
||||
val startOffset: Int,
|
||||
val endIndex: Int,
|
||||
val endItemId: Long,
|
||||
val endOffset: Int
|
||||
)
|
||||
|
||||
@@ -80,11 +82,13 @@ class SelectionManager {
|
||||
var viewportPosition by mutableStateOf(Offset.Zero)
|
||||
var focusCharRect by mutableStateOf(Rect.Zero) // X: absolute window, Y: relative to item
|
||||
var listState: State<LazyListState>? = null
|
||||
var mergedItemsState: State<MergedItems>? = null
|
||||
var onCopySelection: (() -> Unit)? = null
|
||||
private var autoScrollJob: Job? = null
|
||||
|
||||
fun startSelection(startIndex: Int, anchorY: Float, anchorX: Float) {
|
||||
range = SelectionRange(startIndex, -1, startIndex, -1)
|
||||
val id = mergedItemsState?.value?.items?.getOrNull(startIndex)?.newest()?.item?.id ?: return
|
||||
range = SelectionRange(startIndex, id, -1, startIndex, id, -1)
|
||||
selectionState = SelectionState.Selecting
|
||||
anchorWindowY = anchorY
|
||||
anchorWindowX = anchorX
|
||||
@@ -97,7 +101,8 @@ class SelectionManager {
|
||||
|
||||
fun updateFocusIndex(index: Int) {
|
||||
val r = range ?: return
|
||||
range = r.copy(endIndex = index)
|
||||
val id = mergedItemsState?.value?.items?.getOrNull(index)?.newest()?.item?.id ?: return
|
||||
range = r.copy(endIndex = index, endItemId = id)
|
||||
}
|
||||
|
||||
fun updateFocusOffset(offset: Int, charRect: Rect = Rect.Zero) {
|
||||
@@ -176,6 +181,15 @@ class SelectionManager {
|
||||
updateFocusIndex(idx)
|
||||
}
|
||||
|
||||
fun resyncIndices() {
|
||||
val r = range ?: return
|
||||
val items = mergedItemsState?.value?.items ?: return
|
||||
val newStartIndex = items.indexOfFirst { it.newest().item.id == r.startItemId }
|
||||
val newEndIndex = items.indexOfFirst { it.newest().item.id == r.endItemId }
|
||||
if (newStartIndex < 0 || newEndIndex < 0) clearSelection()
|
||||
else range = r.copy(startIndex = newStartIndex, endIndex = newEndIndex)
|
||||
}
|
||||
|
||||
fun updateAutoScroll(draggingDown: Boolean, pointerY: Float, scope: CoroutineScope) {
|
||||
val edgeDistance = if (draggingDown) viewportBottom - pointerY else pointerY - viewportTop
|
||||
if (edgeDistance !in 0f..AUTO_SCROLL_ZONE_PX) {
|
||||
@@ -320,11 +334,15 @@ fun BoxScope.SelectionHandler(
|
||||
}
|
||||
|
||||
manager.listState = listState
|
||||
manager.mergedItemsState = mergedItems
|
||||
manager.onCopySelection = {
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, revealedItems.value, linkMode)))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
|
||||
// Resync after the items list mutates (new message arrives, item deleted).
|
||||
SideEffect { manager.resyncIndices() }
|
||||
|
||||
return Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusable()
|
||||
|
||||
Reference in New Issue
Block a user