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.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(
|
||||||
|
|||||||
Reference in New Issue
Block a user