1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3298 Chats | Tech | Layout optimizations (#2378)

This commit is contained in:
Evgenii Kozlov 2025-05-05 23:05:23 +02:00 committed by GitHub
parent 011f153297
commit 4e56e21447
Signed by: github
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 52 deletions

View file

@ -959,7 +959,7 @@ class ChatViewModel @Inject constructor(
from: Id,
to: Id
) {
Timber.d("onVisibleRangeChanged, from: $from, to: $to")
Timber.d("DROID-2966 onVisibleRangeChanged, from: $from, to: $to")
visibleRangeUpdates.tryEmit(from to to)
}

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.domain.chats.ChatContainer
import com.anytypeio.anytype.feature_chats.R
import com.anytypeio.anytype.feature_chats.presentation.ChatView
import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel
@ -146,31 +147,31 @@ fun ChatPreview2() {
@Composable
fun ChatScreenPreview() {
ChatScreen(
uiMessageState = ChatViewState(
messages = buildList {
repeat(30) { idx ->
add(
ChatView.Message(
id = idx.toString(),
content = ChatView.Message.Content(
msg = stringResource(id = R.string.default_text_placeholder),
parts = listOf(
ChatView.Message.Content.Part(
part = stringResource(id = R.string.default_text_placeholder)
)
messages = buildList {
repeat(30) { idx ->
add(
ChatView.Message(
id = idx.toString(),
content = ChatView.Message.Content(
msg = stringResource(id = R.string.default_text_placeholder),
parts = listOf(
ChatView.Message.Content.Part(
part = stringResource(id = R.string.default_text_placeholder)
)
),
author = "User ${idx.inc()}",
timestamp =
System.currentTimeMillis()
- 30.toDuration(DurationUnit.DAYS).inWholeMilliseconds
+ idx.toDuration(DurationUnit.DAYS).inWholeMilliseconds,
creator = "random id"
)
)
),
author = "User ${idx.inc()}",
timestamp =
System.currentTimeMillis()
- 30.toDuration(DurationUnit.DAYS).inWholeMilliseconds
+ idx.toDuration(DurationUnit.DAYS).inWholeMilliseconds,
creator = "random id"
)
}
}.reversed()
),
)
}
}.reversed(),
counter = ChatViewState.Counter(),
intent = ChatContainer.Intent.None,
onMessageSent = { a, b -> },
attachments = emptyList(),
onClearAttachmentClicked = {},

View file

@ -127,9 +127,25 @@ fun ChatScreenWrapper(
val clipboard = LocalClipboardManager.current
val lazyListState = rememberLazyListState()
val messages by vm.uiState
.map { it.messages }
.collectAsStateWithLifecycle(emptyList())
val counter by vm.uiState
.map { it.counter }
.collectAsStateWithLifecycle(ChatViewState.Counter())
val intent by vm.uiState
.map { it.intent }
.collectAsStateWithLifecycle(ChatContainer.Intent.None)
val mentionPanelState by vm.mentionPanelState.collectAsStateWithLifecycle()
ChatScreen(
chatBoxMode = vm.chatBoxMode.collectAsState().value,
uiMessageState = vm.uiState.collectAsState().value,
messages = messages,
counter = counter,
intent = intent,
attachments = vm.chatBoxAttachments.collectAsState().value,
onMessageSent = { text, spans ->
vm.onMessageSent(
@ -192,7 +208,7 @@ fun ChatScreenWrapper(
onViewChatReaction = onViewChatReaction,
onMemberIconClicked = vm::onMemberIconClicked,
onMentionClicked = vm::onMentionClicked,
mentionPanelState = vm.mentionPanelState.collectAsStateWithLifecycle().value,
mentionPanelState = mentionPanelState,
onTextChanged = { value ->
vm.onChatBoxInputChanged(
selection = value.selection.start..value.selection.end,
@ -288,7 +304,9 @@ fun ChatScreen(
mentionPanelState: MentionPanelState,
chatBoxMode: ChatBoxMode,
lazyListState: LazyListState,
uiMessageState: ChatViewState,
messages: List<ChatView>,
counter: ChatViewState.Counter,
intent: ChatContainer.Intent,
attachments: List<ChatView.Message.ChatBoxAttachment>,
onMessageSent: (String, List<ChatBoxSpan>) -> Unit,
onClearAttachmentClicked: (ChatView.Message.ChatBoxAttachment) -> Unit,
@ -317,7 +335,7 @@ fun ChatScreen(
onVisibleRangeChanged: (Id, Id) -> Unit
) {
Timber.d("DROID-2966 Render called with state, number of messages: ${uiMessageState.messages.size}")
Timber.d("DROID-2966 Render called with state, number of messages: ${messages.size}")
var text by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
@ -329,16 +347,14 @@ fun ChatScreen(
val scope = rememberCoroutineScope()
val latestMessages by rememberUpdatedState(uiMessageState.messages)
val isPerformingScrollIntent = remember { mutableStateOf(false) }
// Applying view model intents
LaunchedEffect(uiMessageState.intent) {
when (val intent = uiMessageState.intent) {
LaunchedEffect(intent) {
when (val intent = intent) {
is ChatContainer.Intent.ScrollToMessage -> {
isPerformingScrollIntent.value = true
val index = uiMessageState.messages.indexOfFirst {
val index = messages.indexOfFirst {
it is ChatView.Message && it.id == intent.id
}
if (index >= 0) {
@ -379,7 +395,7 @@ fun ChatScreen(
isFullyVisible
}
.sortedBy { it.index } // still necessary
.mapNotNull { item -> latestMessages.getOrNull(item.index) }
.mapNotNull { item -> messages.getOrNull(item.index) }
.filterIsInstance<ChatView.Message>()
if (visibleMessages.isNotEmpty() && !isPerformingScrollIntent.value) {
@ -393,24 +409,29 @@ fun ChatScreen(
}
}
val isAtBottom by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex == 0 &&
lazyListState.firstVisibleItemScrollOffset == 0
}
}
// Scrolling to bottom when list size changes and we are at the bottom of the list
LaunchedEffect(latestMessages) {
if (lazyListState.firstVisibleItemScrollOffset == 0 && !isPerformingScrollIntent.value) {
scope.launch {
lazyListState.animateScrollToItem(0)
}
LaunchedEffect(messages.size) {
if (isAtBottom && !isPerformingScrollIntent.value) {
lazyListState.animateScrollToItem(0)
}
}
lazyListState.OnBottomReachedSafely {
if (!isPerformingScrollIntent.value && latestMessages.isNotEmpty()) {
if (!isPerformingScrollIntent.value && messages.isNotEmpty()) {
Timber.d("DROID-2966 Safe onBottomReached dispatched from compose to VM")
onChatScrolledToBottom()
}
}
lazyListState.OnTopReachedSafely {
if (!isPerformingScrollIntent.value && latestMessages.isNotEmpty()) {
if (!isPerformingScrollIntent.value && messages.isNotEmpty()) {
Timber.d("DROID-2966 Safe onTopReached dispatched from compose to VM")
onChatScrolledToTop()
}
@ -422,7 +443,7 @@ fun ChatScreen(
Box(modifier = Modifier.weight(1f)) {
Messages(
modifier = Modifier.fillMaxSize(),
messages = uiMessageState.messages,
messages = messages,
scrollState = lazyListState,
onReacted = onReacted,
onCopyMessage = onCopyMessage,
@ -470,7 +491,7 @@ fun ChatScreen(
onGoToBottomClicked = {
val lastVisibleIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
val lastVisibleView = if (lastVisibleIndex != null) {
latestMessages.getOrNull(lastVisibleIndex)
messages.getOrNull(lastVisibleIndex)
} else {
null
}
@ -485,7 +506,7 @@ fun ChatScreen(
enabled = jumpToBottomButtonEnabled
)
if (uiMessageState.counter.count > 0) {
if (counter.count > 0) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
@ -497,7 +518,7 @@ fun ChatScreen(
)
) {
Text(
text = uiMessageState.counter.count.toString(),
text = counter.count.toString(),
modifier = Modifier.align(Alignment.Center).padding(
horizontal = 5.dp,
vertical = 2.dp
@ -603,9 +624,16 @@ fun ChatScreen(
onMessageSent(text, markup)
},
resetScroll = {
if (lazyListState.firstVisibleItemScrollOffset > 0) {
if (!isPerformingScrollIntent.value) {
scope.launch {
lazyListState.animateScrollToItem(index = 0)
lazyListState.scrollToItem(0)
awaitFrame()
while (!isAtBottom) {
val offset = lazyListState.firstVisibleItemScrollOffset
val delta = (-offset).coerceAtLeast(-80)
lazyListState.animateScrollBy(delta.toFloat())
awaitFrame()
}
}
}
},
@ -723,11 +751,14 @@ fun Messages(
},
reply = msg.reply,
onScrollToReplyClicked = { reply ->
val idx = messages.indexOfFirst { it is ChatView.Message && it.id == reply.msg }
if (idx != -1) {
scope.launch { scrollState.animateScrollToItem(index = idx) }
} else {
onScrollToReplyClicked(reply.msg)
val targetIndex = messages.indexOfFirst { it is ChatView.Message && it.id == reply.msg }
scope.launch {
if (targetIndex != -1 && targetIndex < scrollState.layoutInfo.totalItemsCount) {
scrollState.animateScrollToItem(index = targetIndex)
} else {
// Defer to VM: message likely not yet in the list (e.g. paged)
onScrollToReplyClicked(reply.msg)
}
}
},
onAddReactionClicked = {