Skip to content

Commit aea7cd5

Browse files
authored
feat(chat): wire RemoteMediator for paged message loading and improve chat open speed (#915)
- Wire ChatMessageRemoteMediator into Pager for backward pagination (older messages on scroll up) with cursor-based paging tokens - Fix mediator APPEND to pass messageId cursor via ByteBuffer encoding - Eagerly load messages in OnChatOpened handler instead of waiting for state roundtrip, so fetch starts during navigation transition - Prefetch first page of messages during feed sync for chats with no cached messages - Hide ContactInfoContainer until messages have loaded to prevent flash - Add hasMessages/getLatestMessageId helpers to ChatMessageDataSource Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 1a1c304 commit aea7cd5

5 files changed

Lines changed: 42 additions & 23 deletions

File tree

apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import kotlinx.coroutines.launch
7070
import javax.inject.Inject
7171
import kotlin.math.min
7272
import kotlin.time.Duration
73+
import kotlin.time.Duration.Companion.milliseconds
7374
import kotlin.time.Duration.Companion.seconds
7475
import kotlin.time.Instant
7576

@@ -277,6 +278,7 @@ internal class ChatViewModel @Inject constructor(
277278
if (chatId != null) {
278279
dispatchEvent(Event.ChatFound(chatId))
279280
chatCoordinator.setActiveChatId(chatId)
281+
chatCoordinator.loadMessages(chatId)
280282
chatCoordinator.dismissNotifications(chatId)
281283
}
282284

@@ -318,15 +320,6 @@ internal class ChatViewModel @Inject constructor(
318320
}
319321
).launchIn(viewModelScope)
320322

321-
// trigger message update fetch on open
322-
stateFlow.map { it.chatId }
323-
.filterNotNull()
324-
.distinctUntilChanged()
325-
.onEach { chatId ->
326-
chatCoordinator.loadMessages(chatId)
327-
}
328-
.launchIn(viewModelScope)
329-
330323
// Advance read pointer when user scrolls to messages
331324
eventFlow
332325
.filterIsInstance<Event.AdvanceReadPointer>()
@@ -541,7 +534,7 @@ internal class ChatViewModel @Inject constructor(
541534
).onSuccess { amount ->
542535
dispatchEvent(Event.SendStateUpdated(success = true))
543536
stateFlow.value.chatId?.let { chatCoordinator.loadMessages(it) }
544-
delay(400)
537+
delay(400.milliseconds)
545538
dispatchEvent(
546539
Dispatchers.Main,
547540
Event.SendComplete(amount.localFiat.nativeAmount)

apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,16 @@ internal fun MessageList(
155155
}
156156
}
157157

158-
// Chat start shows contact info container
159-
item {
160-
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.Center) {
161-
ContactInfoContainer(
162-
contact = state.chattingWith,
163-
modifier = Modifier
164-
.padding(horizontal = CodeTheme.dimens.grid.x12)
165-
)
158+
// Chat start shows contact info container (only after messages have loaded)
159+
if (messages.itemCount > 0) {
160+
item {
161+
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.Center) {
162+
ContactInfoContainer(
163+
contact = state.chattingWith,
164+
modifier = Modifier
165+
.padding(horizontal = CodeTheme.dimens.grid.x12)
166+
)
167+
}
166168
}
167169
}
168170
}

apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
@file:OptIn(ExperimentalCoroutinesApi::class)
1+
@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalPagingApi::class)
22

33
package com.flipcash.shared.chat
44

55
import androidx.core.app.NotificationManagerCompat
66
import androidx.lifecycle.DefaultLifecycleObserver
77
import androidx.lifecycle.LifecycleOwner
88
import androidx.lifecycle.ProcessLifecycleOwner
9+
import androidx.paging.ExperimentalPagingApi
910
import androidx.paging.Pager
1011
import androidx.paging.PagingConfig
1112
import androidx.paging.PagingData
1213
import androidx.paging.map
1314
import com.flipcash.app.contacts.device.DeviceContact
1415
import com.flipcash.app.persistence.sources.ChatMemberDataSource
16+
import com.flipcash.app.persistence.sources.mediator.ChatMessageRemoteMediator
1517
import com.flipcash.app.persistence.sources.ChatMessageDataSource
1618
import com.flipcash.app.persistence.sources.ChatMetadataDataSource
1719
import com.flipcash.app.persistence.sources.ContactDataSource
@@ -173,6 +175,7 @@ class ChatCoordinator @Inject constructor(
173175
fun observeMessagesPaged(chatId: ChatId): Flow<PagingData<ChatMessage>> {
174176
return Pager(
175177
config = PagingConfig(pageSize = 50),
178+
remoteMediator = ChatMessageRemoteMediator(chatId, messagingController, messageDataSource),
176179
) {
177180
messageDataSource.observeForChat(chatId)
178181
}.flow.map { page ->
@@ -195,7 +198,7 @@ class ChatCoordinator @Inject constructor(
195198
.distinctUntilChanged()
196199
}
197200

198-
suspend fun loadMessages(chatId: ChatId, limit: Int = 100) {
201+
suspend fun loadMessages(chatId: ChatId) {
199202
messagingController.getMessages(chatId)
200203
.onSuccess { messages ->
201204
messageDataSource.upsert(chatId, messages)
@@ -335,6 +338,11 @@ class ChatCoordinator @Inject constructor(
335338

336339
_state.update { it.copy(feed = page.chats, feedSyncState = FeedSyncState.Synced) }
337340
trace(tag = TAG, message = "Feed synced: ${page.chats.size} chats", type = TraceType.Process)
341+
342+
// Prefetch first page of messages for chats with no cached messages
343+
page.chats
344+
.filterNot { messageDataSource.hasMessages(it.chatId) }
345+
.forEach { chat -> loadMessages(chat.chatId) }
338346
}
339347
.onFailure { error ->
340348
_state.update { it.copy(feedSyncState = FeedSyncState.Error) }

apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMessageDataSource.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ class ChatMessageDataSource @Inject constructor(
9292
suspend fun getLatest(chatIdHex: String): ChatMessage? =
9393
db?.chatMessageDao()?.getLatest(chatIdHex)?.let { toChatMessage(it) }
9494

95+
suspend fun hasMessages(chatId: ChatId): Boolean =
96+
db?.chatMessageDao()?.getLatest(mapper.chatIdHex(chatId)) != null
97+
98+
suspend fun getLatestMessageId(chatId: ChatId): Long? =
99+
db?.chatMessageDao()?.getLatest(mapper.chatIdHex(chatId))?.messageId
100+
95101
suspend fun upsert(chatId: ChatId, messages: List<ChatMessage>) {
96102
val hex = mapper.chatIdHex(chatId)
97103
val entities = messages.map { mapper.toEntity(hex, it) }

apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mediator/ChatMessageRemoteMediator.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.flipcash.services.models.QueryOptions
1111
import com.flipcash.services.models.chat.ChatId
1212
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.withContext
14+
import java.nio.ByteBuffer
1415

1516
@OptIn(ExperimentalPagingApi::class)
1617
class ChatMessageRemoteMediator(
@@ -28,14 +29,19 @@ class ChatMessageRemoteMediator(
2829
state: PagingState<Int, ChatMessageEntity>,
2930
): MediatorResult {
3031
return try {
31-
when (loadType) {
32-
LoadType.REFRESH -> Unit
32+
val token = when (loadType) {
33+
LoadType.REFRESH -> null
3334
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
34-
LoadType.APPEND -> Unit
35+
LoadType.APPEND -> {
36+
val lastItem = state.lastItemOrNull()
37+
?: return MediatorResult.Success(endOfPaginationReached = true)
38+
lastItem.messageId.toPagingToken()
39+
}
3540
}
3641

3742
val queryOptions = QueryOptions(
3843
limit = state.config.pageSize,
44+
token = token,
3945
descending = true,
4046
)
4147

@@ -52,3 +58,7 @@ class ChatMessageRemoteMediator(
5258
}
5359
}
5460
}
61+
62+
private fun Long.toPagingToken(): List<Byte> {
63+
return ByteBuffer.allocate(Long.SIZE_BYTES).putLong(this).array().toList()
64+
}

0 commit comments

Comments
 (0)