From e951816fd0e86e2ec27e03fc6e36f58e17df5be4 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:42:25 +0000 Subject: [PATCH] android, desktop: pinned sender avatar follows member updates --- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 7 +------ plans/2026-10-02-sticky-sender-avatar.md | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index a154f2567c..940ad2eb5d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2513,7 +2513,7 @@ fun BoxScope.ChatItemsList( return mergedItems.value.items.getOrNull(index)?.let { index to it } } if (selectedChatItems.value != null) return@pinned - val memberAtTop = remember { derivedStateOf(sameMemberPolicy) { topItem()?.let { (it.second.newest().item.chatDir as? CIDirection.GroupRcv)?.groupMember } } } + val memberAtTop = remember { derivedStateOf { topItem()?.let { (it.second.newest().item.chatDir as? CIDirection.GroupRcv)?.groupMember } } } val member = memberAtTop.value ?: return@pinned Box( Modifier @@ -3862,11 +3862,6 @@ private fun shouldShowAvatar(current: ChatItem, older: ChatItem?): Boolean { } } -// messages of one sender can carry different snapshots of the member -private val sameMemberPolicy = object : SnapshotMutationPolicy { - override fun equivalent(a: GroupMember?, b: GroupMember?): Boolean = a?.memberId == b?.memberId -} - // the list under the stand-in avatar still gets the drags and wheel events that scroll it private data object ShareTouchesWithList : ModifierNodeElement() { override fun create(): Modifier.Node = object : Modifier.Node(), PointerInputModifierNode { diff --git a/plans/2026-10-02-sticky-sender-avatar.md b/plans/2026-10-02-sticky-sender-avatar.md index 75ac5e951d..8cd32cb270 100644 --- a/plans/2026-10-02-sticky-sender-avatar.md +++ b/plans/2026-10-02-sticky-sender-avatar.md @@ -19,7 +19,7 @@ One rule on both platforms: the avatar's top is `max(naturalTop, min(belowTopBar ### Android and desktop (Compose) - **The run's first row moves its own avatar.** A layout modifier on the row's avatar reads its `coordinates` during placement. Compose re-runs such placement whenever an ancestor moves (`notifyChildrenUsingCoordinatesWhilePlacing`), so the avatar follows the scroll in the same frame, with no overlay and no extra state. The avatar draws outside its row; rows are not clipped and older rows are placed after newer ones, so it is drawn and hit-tested above the rows it covers. It stays clickable as before. -- **A stand-in when that row is gone.** LazyColumn disposes the first row once it scrolls out, but the run can be taller than the screen. An overlay above the list draws the same avatar at the same position only while the run's first row is not in `visibleItemsInfo`, so exactly one of the two is drawn and the hand-off is seamless. It is composed per member (compared by `memberId`, since messages of one sender carry different member snapshots) in a composable lambda, which has its own restart scope, so its reads don't recompose the chat list; it is shown or hidden during placement, which runs in the same frame as the list. +- **A stand-in when that row is gone.** LazyColumn disposes the first row once it scrolls out, but the run can be taller than the screen. An overlay above the list draws the same avatar at the same position only while the run's first row is not in `visibleItemsInfo`, so exactly one of the two is drawn and the hand-off is seamless. It is composed with the top message's member (compared structurally, so a member update such as blocking or a new picture is drawn and passed to member info, and only a hand-off between messages carrying different member snapshots recomposes it) in a composable lambda, which has its own restart scope, so its reads don't recompose the chat list; it is shown or hidden during placement, which runs in the same frame as the list. - **The stand-in is clickable without blocking scrolling.** A no-op pointer-input node with `sharePointerInputWithSiblings() = true` lets the list under it still receive drags and wheel events. - Run geometry (`senderRunBottom`) only walks visible items, so the per-frame cost does not depend on run length.