Fix message loading

This commit is contained in:
2026-04-19 17:41:08 +03:00
parent c83b3e455f
commit b3b2796d25
@@ -83,6 +83,7 @@ import de.connect2x.trixnity.client.store.TimelineEvent
import de.connect2x.trixnity.client.store.eventId import de.connect2x.trixnity.client.store.eventId
import de.connect2x.trixnity.core.model.RoomId import de.connect2x.trixnity.core.model.RoomId
import de.connect2x.trixnity.core.model.events.ClientEvent import de.connect2x.trixnity.core.model.events.ClientEvent
import de.connect2x.trixnity.core.model.events.m.FullyReadEventContent
import de.connect2x.trixnity.core.model.events.m.room.AvatarEventContent import de.connect2x.trixnity.core.model.events.m.room.AvatarEventContent
import de.connect2x.trixnity.core.model.events.m.room.CanonicalAliasEventContent import de.connect2x.trixnity.core.model.events.m.room.CanonicalAliasEventContent
import de.connect2x.trixnity.core.model.events.m.room.HistoryVisibilityEventContent import de.connect2x.trixnity.core.model.events.m.room.HistoryVisibilityEventContent
@@ -101,6 +102,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@@ -117,6 +119,10 @@ import kotlin.math.roundToInt
private const val WEBVIEW_HEIGHT_TITLE_PREFIX = "pd-height:" private const val WEBVIEW_HEIGHT_TITLE_PREFIX = "pd-height:"
private const val MESSAGE_GROUP_WINDOW_MS = 5 * 60 * 1000L private const val MESSAGE_GROUP_WINDOW_MS = 5 * 60 * 1000L
private const val TIMELINE_PAGE_SIZE = 50
private const val TIMELINE_TOP_PREFETCH_THRESHOLD = 4
private const val TIMELINE_BOTTOM_AUTOSCROLL_THRESHOLD = 3
private const val MAX_INITIAL_UNREAD_SEARCH_LIMIT = 500
private val TIMELINE_AVATAR_SIZE = 32.dp private val TIMELINE_AVATAR_SIZE = 32.dp
private val MESSAGE_FIELD_TOP_PADDING = 8.dp private val MESSAGE_FIELD_TOP_PADDING = 8.dp
@@ -182,9 +188,14 @@ private val headingMetrics = mapOf(
@Composable @Composable
fun Room(modifier: Modifier = Modifier, rid: String) { fun Room(modifier: Modifier = Modifier, rid: String) {
val roomId = remember(rid) { RoomId(rid) } val roomId = remember(rid) { RoomId(rid) }
val limit = remember { MutableStateFlow(50) } var requestedLimit by remember(roomId) { mutableStateOf(TIMELINE_PAGE_SIZE) }
val limit = remember(roomId) { MutableStateFlow(TIMELINE_PAGE_SIZE) }
val eventsFlow = remember(roomId) { LaunchedEffect(requestedLimit) {
limit.value = requestedLimit
}
val eventsFlow = remember(roomId, client!!) {
client!!.room client!!.room
.getLastTimelineEvents(roomId) .getLastTimelineEvents(roomId)
.toFlowList(limit) .toFlowList(limit)
@@ -194,23 +205,105 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
} }
} }
val events by eventsFlow.collectAsState(initial = emptyList()) val events by eventsFlow.collectAsState(initial = emptyList())
val currentUserId = remember { client!!.userId.toString() }
val timelineItems = remember(events, currentUserId) { val readMarkerFlow = remember(roomId, client!!) {
buildTimelineItems(events = events, currentUserId = currentUserId) client!!.room
.getAccountData(roomId, FullyReadEventContent::class)
.map { content -> ReadMarkerState.Ready(content?.eventId?.full) as ReadMarkerState }
}
val readMarkerState by readMarkerFlow.collectAsState(initial = ReadMarkerState.Loading)
val fullyReadEventId = (readMarkerState as? ReadMarkerState.Ready)?.eventId
val currentUserId = remember(client!!) { client!!.userId.toString() }
val timelineItems = remember(events, currentUserId, fullyReadEventId) {
buildTimelineItems(
events = events,
currentUserId = currentUserId,
fullyReadEventId = fullyReadEventId,
)
} }
val latestTimelineKey = remember(timelineItems) { val latestTimelineKey = remember(timelineItems) {
timelineItems.lastOrNull()?.latestKey() timelineItems.lastOrNull()?.latestKey()
} }
val listState = rememberLazyListState() val listState = rememberLazyListState()
var didInitialScroll by remember(roomId) { mutableStateOf(false) }
var lastAutoScrollTimelineKey by remember(roomId) { mutableStateOf<String?>(null) }
val isNearBottom by remember(timelineItems, listState) {
derivedStateOf {
val totalItems = listState.layoutInfo.totalItemsCount
if (totalItems == 0) return@derivedStateOf true
val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: return@derivedStateOf false
lastVisibleIndex >= totalItems - 1 - TIMELINE_BOTTOM_AUTOSCROLL_THRESHOLD
}
}
val shouldLoadOlder by remember(listState, didInitialScroll) {
derivedStateOf {
didInitialScroll &&
listState.layoutInfo.totalItemsCount > 0 &&
listState.firstVisibleItemIndex <= TIMELINE_TOP_PREFETCH_THRESHOLD
}
}
Column(modifier) { Column(modifier) {
var message by remember { mutableStateOf("") } var message by remember { mutableStateOf("") }
LaunchedEffect(latestTimelineKey) { LaunchedEffect(timelineItems, readMarkerState, events.size, requestedLimit) {
if (timelineItems.isNotEmpty()) { if (didInitialScroll || timelineItems.isEmpty()) return@LaunchedEffect
listState.scrollToItem(timelineItems.lastIndex) if (readMarkerState is ReadMarkerState.Loading) return@LaunchedEffect
val readMarkerEventId = (readMarkerState as ReadMarkerState.Ready).eventId
val unreadDividerIndex = timelineItems.indexOfFirst { it is UnreadDividerItem }
if (unreadDividerIndex >= 0) {
listState.scrollToItem(unreadDividerIndex)
didInitialScroll = true
lastAutoScrollTimelineKey = latestTimelineKey
return@LaunchedEffect
} }
val readMarkerIsLoaded = readMarkerEventId != null &&
events.any { it.eventId.full == readMarkerEventId }
val shouldSearchOlderForUnreadMarker = readMarkerEventId != null &&
!readMarkerIsLoaded &&
events.size >= requestedLimit &&
requestedLimit < MAX_INITIAL_UNREAD_SEARCH_LIMIT
if (shouldSearchOlderForUnreadMarker) {
requestedLimit = (requestedLimit + TIMELINE_PAGE_SIZE)
.coerceAtMost(MAX_INITIAL_UNREAD_SEARCH_LIMIT)
return@LaunchedEffect
}
listState.scrollToItem(timelineItems.lastIndex)
didInitialScroll = true
lastAutoScrollTimelineKey = latestTimelineKey
}
LaunchedEffect(shouldLoadOlder, events.size, requestedLimit) {
if (!shouldLoadOlder) return@LaunchedEffect
if (events.size < requestedLimit) return@LaunchedEffect
requestedLimit += TIMELINE_PAGE_SIZE
}
LaunchedEffect(latestTimelineKey) {
val currentLatestKey = latestTimelineKey ?: return@LaunchedEffect
val previousLatestKey = lastAutoScrollTimelineKey
if (!didInitialScroll) {
lastAutoScrollTimelineKey = currentLatestKey
return@LaunchedEffect
}
if (previousLatestKey != null && previousLatestKey != currentLatestKey && isNearBottom) {
listState.animateScrollToItem(timelineItems.lastIndex)
}
lastAutoScrollTimelineKey = currentLatestKey
} }
val density = LocalDensity.current val density = LocalDensity.current
@@ -297,6 +390,11 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
} }
} }
private sealed interface ReadMarkerState {
data object Loading : ReadMarkerState
data class Ready(val eventId: String?) : ReadMarkerState
}
private sealed interface TimelineUiItem { private sealed interface TimelineUiItem {
val key: String val key: String
} }
@@ -306,6 +404,10 @@ private data class DateDividerItem(
val label: String, val label: String,
) : TimelineUiItem ) : TimelineUiItem
private data class UnreadDividerItem(
override val key: String,
) : TimelineUiItem
private data class MessageGroupItem( private data class MessageGroupItem(
override val key: String, override val key: String,
val senderId: String?, val senderId: String?,
@@ -338,6 +440,7 @@ private data class SystemTimelineItem(
private fun buildTimelineItems( private fun buildTimelineItems(
events: List<TimelineEvent>, events: List<TimelineEvent>,
currentUserId: String, currentUserId: String,
fullyReadEventId: String?,
): List<TimelineUiItem> { ): List<TimelineUiItem> {
if (events.isEmpty()) return emptyList() if (events.isEmpty()) return emptyList()
@@ -346,6 +449,8 @@ private fun buildTimelineItems(
var previousDayStartMs: Long? = null var previousDayStartMs: Long? = null
var lastMessageGroupIndex: Int? = null var lastMessageGroupIndex: Int? = null
var insertUnreadDividerBeforeNextMessage = false
var unreadDividerInserted = false
for (event in chronologicalEvents) { for (event in chronologicalEvents) {
val content = event.content?.getOrNull() val content = event.content?.getOrNull()
@@ -361,6 +466,17 @@ private fun buildTimelineItems(
lastMessageGroupIndex = null lastMessageGroupIndex = null
} }
if (
content is RoomMessageEventContent &&
insertUnreadDividerBeforeNextMessage &&
!unreadDividerInserted
) {
items += UnreadDividerItem(key = "unread:$fullyReadEventId")
unreadDividerInserted = true
insertUnreadDividerBeforeNextMessage = false
lastMessageGroupIndex = null
}
when { when {
content == null -> { content == null -> {
items += SystemTimelineItem( items += SystemTimelineItem(
@@ -429,6 +545,11 @@ private fun buildTimelineItems(
lastMessageGroupIndex = null lastMessageGroupIndex = null
} }
} }
if (!unreadDividerInserted && fullyReadEventId != null && event.eventId.full == fullyReadEventId) {
insertUnreadDividerBeforeNextMessage = true
lastMessageGroupIndex = null
}
} }
return items return items
@@ -441,6 +562,7 @@ private fun TimelineItem(
) { ) {
when (item) { when (item) {
is DateDividerItem -> DateDivider(item) is DateDividerItem -> DateDivider(item)
is UnreadDividerItem -> UnreadDivider()
is MessageGroupItem -> MessageGroupRow( is MessageGroupItem -> MessageGroupRow(
group = item, group = item,
hideInlineAvatar = pinnedAvatarGroupKey == item.key, hideInlineAvatar = pinnedAvatarGroupKey == item.key,
@@ -472,6 +594,36 @@ private fun DateDivider(item: DateDividerItem) {
} }
} }
@Composable
private fun UnreadDivider() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.height(1.dp)
.weight(1f)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.45f)),
)
Text(
text = "Unread messages",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold,
)
Box(
modifier = Modifier
.height(1.dp)
.weight(1f)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.45f)),
)
}
}
@Composable @Composable
private fun SystemEventRow(item: SystemTimelineItem) { private fun SystemEventRow(item: SystemTimelineItem) {
Text( Text(