Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,6 @@ sealed interface ConversationListUiAction {
val conversationId: Uuid,
val imageBytes: ByteArray,
) : ConversationListUiAction

data class LoadMoreMessages(val conversationId: Uuid) : ConversationListUiAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ data class MessageListUiState(
val userDefaultReactions: ImmutableList<String> = persistentListOf(),
val uploadProgress: ImmutableMap<Uuid, UploadStatus> = persistentMapOf(),
val isLoadingMessages: Boolean = true,
val hasMoreMessages: Boolean = false,
val isLoadingMoreMessages: Boolean = false,
val scrollPosition: ScrollPosition? = null,
val fullScreenOverlay: FullScreenOverlay? = null,
val replyToMessage: MessageUiModel? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,24 @@ class ConversationListViewModel(
}
}

is ConversationListUiAction.LoadMoreMessages -> {
val conversationId = action.conversationId
if (_messagesUiState.value.isLoadingMoreMessages) return
_messagesUiState.update { it.copy(isLoadingMoreMessages = true) }
viewModelScope.launch {
try {
chatMessageStream.loadMoreMessages(conversationId)
} finally {
_messagesUiState.update {
it.copy(
isLoadingMoreMessages = false,
hasMoreMessages = chatMessageStream.hasMoreMessages(conversationId)
)
}
}
}
}

is ConversationListUiAction.EditMessage -> {
viewModelScope.launch {
try {
Expand Down Expand Up @@ -1654,6 +1672,7 @@ class ConversationListViewModel(
_messagesUiState.update {
it.copy(
isLoadingMessages = false,
hasMoreMessages = chatMessageStream.hasMoreMessages(conversationId),
messages = messagesModels.toPersistentList(),
scrollPosition = if (indexOfMessageForScroll == null) {
if (setInitialScroll) getScrollPosition(conversationId) else null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class ChatMessageStream(
private val chatDrive = chatTargetDrive.alias
private var isSyncing = false
private val loadedConversations = mutableSetOf<Uuid>()
private val conversationCursors = mutableMapOf<Uuid, QueryBatchCursor>()
private val conversationHasMore = mutableMapOf<Uuid, Boolean>()

init {
scope.launch {
Expand Down Expand Up @@ -105,12 +107,27 @@ class ChatMessageStream(
suspend fun loadConversation(conversationId: Uuid) {
Logger.d("ChatMessageStream: loadConversation($conversationId)")
loadedConversations += conversationId
val result = fetchMessages(conversationId)
Logger.d("ChatMessageStream: loadConversation($conversationId) → ${result.records.size} messages")
val result = fetchMessages(conversationId, limit = INITIAL_MESSAGE_LOAD)
Logger.d("ChatMessageStream: loadConversation($conversationId) → ${result.records.size} messages, hasMore=${result.hasMoreRows}")
conversationCursors[conversationId] = result.cursor
conversationHasMore[conversationId] = result.hasMoreRows
conversationState.set(conversationId, result.records)
}

// ---------- EVENT HANDLING ----------
fun hasMoreMessages(conversationId: Uuid): Boolean = conversationHasMore[conversationId] == true

suspend fun loadMoreMessages(conversationId: Uuid) {
if (!hasMoreMessages(conversationId)) return
val cursor = conversationCursors[conversationId]
Logger.d("ChatMessageStream: loadMoreMessages($conversationId)")
val result = fetchMessages(conversationId, limit = SUBSEQUENT_MESSAGE_LOAD, cursor = cursor)
Logger.d("ChatMessageStream: loadMoreMessages($conversationId) → ${result.records.size} messages, hasMore=${result.hasMoreRows}")
conversationCursors[conversationId] = result.cursor
conversationHasMore[conversationId] = result.hasMoreRows
conversationState.upsert(conversationId, result.records)
}

// ---------- EVENT HANDLING ----------

private suspend fun processIncrementalBatch(files: List<HomebaseFile>) {
val messages =
Expand All @@ -126,8 +143,9 @@ class ChatMessageStream(
private suspend fun refreshLoadedConversations() {
Logger.d("ChatMessageStream: refreshLoadedConversations called, ${loadedConversations.size} active conversations")
loadedConversations.forEach { conversationId ->
val result = fetchMessages(conversationId)
Logger.d("ChatMessageStream: fetchMessages($conversationId) → ${result.records.size} messages")
val result = fetchMessages(conversationId, limit = INITIAL_MESSAGE_LOAD)
conversationCursors[conversationId] = result.cursor
conversationHasMore[conversationId] = result.hasMoreRows
conversationState.set(conversationId, result.records)
}
}
Expand Down Expand Up @@ -339,6 +357,8 @@ class ChatMessageStream(
}

companion object {
const val INITIAL_MESSAGE_LOAD = 50
const val SUBSEQUENT_MESSAGE_LOAD = 500

private fun getDeliveryStatus(header: HomebaseFile): ChatDeliveryStatus {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,35 @@ fun ConversationMessagesPane(
}
}

// Trigger load-more when user scrolls near the top (older messages)
LaunchedEffect(listState, uiState.hasMoreMessages, uiState.isLoadingMoreMessages) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index ->
if (index <= 5 && uiState.hasMoreMessages && !uiState.isLoadingMoreMessages) {
onUiAction(ConversationListUiAction.LoadMoreMessages(conversation.conversation.id))
}
}
}

// Compensate scroll position after older messages are prepended
var prevIsLoadingMore by remember { mutableStateOf(false) }
var itemCountAtLoadStart by remember { mutableStateOf(0) }
LaunchedEffect(uiState.isLoadingMoreMessages) {
if (uiState.isLoadingMoreMessages) {
itemCountAtLoadStart = listState.layoutInfo.totalItemsCount
} else if (prevIsLoadingMore) {
val added = listState.layoutInfo.totalItemsCount - itemCountAtLoadStart
if (added > 0) {
listState.scrollToItem(
listState.firstVisibleItemIndex + added,
listState.firstVisibleItemScrollOffset
)
}
}
prevIsLoadingMore = uiState.isLoadingMoreMessages
}

val seen = remember(conversation.conversation.id) { mutableSetOf<Uuid>() }
val messageIdByKey = remember(uiState.messages) {
uiState.messages.associateNotNull { item ->
Expand Down
Loading