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 8dd3e42440..2aee98acba 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 @@ -980,8 +980,9 @@ fun BoxScope.ChatItemsList( } val chatInfoUpdated = rememberUpdatedState(chatInfo) + val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) SmallScrollOnNewMessage(listState, chatModel.chatItems) @@ -1031,7 +1032,17 @@ fun BoxScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } + LaunchedEffect(Unit) { + snapshotFlow { highlighted.value } + .distinctUntilChanged() + .filter { it } + .collect { + delay(500) + highlightedItems.value = setOf() + } + } + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1810,6 +1821,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State, de private fun scrollToItem( loadingMoreItems: MutableState, + highlightedItems: MutableState>, chatInfo: State, maxHeight: State, scope: CoroutineScope, @@ -1840,8 +1852,13 @@ private fun scrollToItem( index = mergedItems.value.indexInParentItems[itemId] ?: -1 } if (index != -1) { - withContext(scope.coroutineContext) { - listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + if (listState.value.layoutInfo.visibleItemsInfo.any { it.index == index && it.offset + it.size <= maxHeight.value }) { + highlightedItems.value = setOf(itemId) + } else { + withContext(scope.coroutineContext) { + listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + highlightedItems.value = setOf(itemId) + } } } } finally { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 5096992c29..f0c85736af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -2,6 +2,8 @@ package chat.simplex.common.views.chat.item import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* @@ -57,6 +59,7 @@ fun ChatItemView( useLinkPreviews: Boolean, linkMode: SimplexLinkMode, revealed: State, + highlighted: State, range: State, selectedChatItems: MutableState?>, fillMaxWidth: Boolean = true, @@ -135,10 +138,19 @@ fun ChatItemView( } Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { + val interactionSource = remember { MutableInteractionSource() } + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + interactionSource.emit(enterInteraction) + } else { + interactionSource.emit(HoverInteraction.Exit(enterInteraction)) + } + } Column( Modifier .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick) + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = interactionSource, indication = LocalIndication.current) .onRightClick { showMenu.value = true }, ) { @Composable @@ -1064,6 +1076,7 @@ fun PreviewChatItemView( linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, + highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, @@ -1106,6 +1119,7 @@ fun PreviewChatItemViewDeletedContent() { linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, + highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {},