Fix message loading
This commit is contained in:
@@ -83,6 +83,7 @@ import de.connect2x.trixnity.client.store.TimelineEvent
|
||||
import de.connect2x.trixnity.client.store.eventId
|
||||
import de.connect2x.trixnity.core.model.RoomId
|
||||
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.CanonicalAliasEventContent
|
||||
import de.connect2x.trixnity.core.model.events.m.room.HistoryVisibilityEventContent
|
||||
@@ -100,6 +101,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -116,6 +118,10 @@ import kotlin.math.roundToInt
|
||||
|
||||
private const val WEBVIEW_HEIGHT_TITLE_PREFIX = "pd-height:"
|
||||
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_START_PADDING = 8.dp
|
||||
private val TIMELINE_AVATAR_BOTTOM_PADDING = 8.dp
|
||||
@@ -182,9 +188,14 @@ private val headingMetrics = mapOf(
|
||||
@Composable
|
||||
fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||
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
|
||||
.getLastTimelineEvents(roomId)
|
||||
.toFlowList(limit)
|
||||
@@ -194,23 +205,105 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||
}
|
||||
}
|
||||
val events by eventsFlow.collectAsState(initial = emptyList())
|
||||
val currentUserId = remember { client!!.userId.toString() }
|
||||
val timelineItems = remember(events, currentUserId) {
|
||||
buildTimelineItems(events = events, currentUserId = currentUserId)
|
||||
|
||||
val readMarkerFlow = remember(roomId, client!!) {
|
||||
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) {
|
||||
timelineItems.lastOrNull()?.latestKey()
|
||||
}
|
||||
|
||||
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) {
|
||||
var message by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(latestTimelineKey) {
|
||||
if (timelineItems.isNotEmpty()) {
|
||||
listState.scrollToItem(timelineItems.lastIndex)
|
||||
LaunchedEffect(timelineItems, readMarkerState, events.size, requestedLimit) {
|
||||
if (didInitialScroll || timelineItems.isEmpty()) return@LaunchedEffect
|
||||
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
|
||||
@@ -298,6 +391,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 {
|
||||
val key: String
|
||||
}
|
||||
@@ -307,6 +405,10 @@ private data class DateDividerItem(
|
||||
val label: String,
|
||||
) : TimelineUiItem
|
||||
|
||||
private data class UnreadDividerItem(
|
||||
override val key: String,
|
||||
) : TimelineUiItem
|
||||
|
||||
private data class MessageGroupItem(
|
||||
override val key: String,
|
||||
val senderId: String?,
|
||||
@@ -339,6 +441,7 @@ private data class SystemTimelineItem(
|
||||
private fun buildTimelineItems(
|
||||
events: List<TimelineEvent>,
|
||||
currentUserId: String,
|
||||
fullyReadEventId: String?,
|
||||
): List<TimelineUiItem> {
|
||||
if (events.isEmpty()) return emptyList()
|
||||
|
||||
@@ -347,6 +450,8 @@ private fun buildTimelineItems(
|
||||
|
||||
var previousDayStartMs: Long? = null
|
||||
var lastMessageGroupIndex: Int? = null
|
||||
var insertUnreadDividerBeforeNextMessage = false
|
||||
var unreadDividerInserted = false
|
||||
|
||||
for (event in chronologicalEvents) {
|
||||
val content = event.content?.getOrNull()
|
||||
@@ -362,6 +467,17 @@ private fun buildTimelineItems(
|
||||
lastMessageGroupIndex = null
|
||||
}
|
||||
|
||||
if (
|
||||
content is RoomMessageEventContent &&
|
||||
insertUnreadDividerBeforeNextMessage &&
|
||||
!unreadDividerInserted
|
||||
) {
|
||||
items += UnreadDividerItem(key = "unread:$fullyReadEventId")
|
||||
unreadDividerInserted = true
|
||||
insertUnreadDividerBeforeNextMessage = false
|
||||
lastMessageGroupIndex = null
|
||||
}
|
||||
|
||||
when {
|
||||
content == null -> {
|
||||
items += SystemTimelineItem(
|
||||
@@ -430,6 +546,11 @@ private fun buildTimelineItems(
|
||||
lastMessageGroupIndex = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!unreadDividerInserted && fullyReadEventId != null && event.eventId.full == fullyReadEventId) {
|
||||
insertUnreadDividerBeforeNextMessage = true
|
||||
lastMessageGroupIndex = null
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
@@ -442,6 +563,7 @@ private fun TimelineItem(
|
||||
) {
|
||||
when (item) {
|
||||
is DateDividerItem -> DateDivider(item)
|
||||
is UnreadDividerItem -> UnreadDivider()
|
||||
is MessageGroupItem -> MessageGroupRow(
|
||||
group = item,
|
||||
hideInlineAvatar = pinnedAvatarGroupKey == item.key,
|
||||
@@ -473,6 +595,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
|
||||
private fun SystemEventRow(item: SystemTimelineItem) {
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user