Quickly branch pagination

This commit is contained in:
ganfra 2022-11-07 18:41:28 +01:00
parent da8b307339
commit bff815e2c5
6 changed files with 91 additions and 47 deletions

View file

@ -8,11 +8,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -33,7 +34,14 @@ fun MessagesScreen(roomId: String) {
val roomTitle by viewModel.collectAsState(MessagesViewState::roomName) val roomTitle by viewModel.collectAsState(MessagesViewState::roomName)
val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar) val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar)
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems) val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
MessagesContent(roomTitle, roomAvatar, timelineItems().orEmpty()) val hasMoreToLoad by viewModel.collectAsState(MessagesViewState::hasMoreToLoad)
MessagesContent(
roomTitle = roomTitle,
roomAvatar = roomAvatar,
timelineItems = timelineItems().orEmpty(),
hasMoreToLoad = hasMoreToLoad,
onReachedLoadMore = viewModel::loadMore
)
} }
@Composable @Composable
@ -41,6 +49,8 @@ fun MessagesContent(
roomTitle: String?, roomTitle: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit,
) { ) {
LogCompositions(tag = "MessagesScreen", msg = "Content") LogCompositions(tag = "MessagesScreen", msg = "Content")
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -61,7 +71,9 @@ fun MessagesContent(
TimelineItems( TimelineItems(
padding = padding, padding = padding,
lazyListState = lazyListState, lazyListState = lazyListState,
timelineItems = timelineItems timelineItems = timelineItems,
hasMoreToLoad = hasMoreToLoad,
onReachedLoadMore = onReachedLoadMore,
) )
} }
) )
@ -71,7 +83,9 @@ fun MessagesContent(
fun TimelineItems( fun TimelineItems(
padding: PaddingValues, padding: PaddingValues,
lazyListState: LazyListState, lazyListState: LazyListState,
timelineItems: List<MatrixTimelineItem> timelineItems: List<MatrixTimelineItem>,
hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit,
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
@ -82,12 +96,18 @@ fun TimelineItems(
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
reverseLayout = true reverseLayout = true
) { ) {
items(timelineItems) { timelineItem -> itemsIndexed(timelineItems) { index, timelineItem ->
TimelineItemRow(timelineItem = timelineItem) TimelineItemRow(timelineItem = timelineItem)
} }
if (hasMoreToLoad) {
item {
MessagesLoadingMoreIndicator(onReachedLoadMore)
}
}
} }
} }
@Composable @Composable
fun TimelineItemRow( fun TimelineItemRow(
timelineItem: MatrixTimelineItem timelineItem: MatrixTimelineItem
@ -124,7 +144,7 @@ fun TimelineItemRow(
} }
@Composable @Composable
internal fun MessagesLoadingMoreIndicator() { internal fun MessagesLoadingMoreIndicator(onReachedLoadMore: () -> Unit) {
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -133,6 +153,10 @@ internal fun MessagesLoadingMoreIndicator() {
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CircularProgressIndicator(strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary) CircularProgressIndicator(strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary)
LaunchedEffect(Unit) {
onReachedLoadMore()
}
} }
} }

View file

@ -9,13 +9,17 @@ import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
private const val PAGINATION_COUNT = 50
class MessagesViewModel( class MessagesViewModel(
private val client: MatrixClient, private val client: MatrixClient,
private val room: MatrixRoom, private val room: MatrixRoom,
private val timeline: MatrixTimeline,
private val initialState: MessagesViewState private val initialState: MessagesViewState
) : ) :
MavericksViewModel<MessagesViewState>(initialState) { MavericksViewModel<MessagesViewState>(initialState) {
@ -29,7 +33,7 @@ class MessagesViewModel(
val matrix = MatrixInstance.getInstance() val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient() val client = matrix.activeClient()
val room = client.getRoom(state.roomId) ?: return null val room = client.getRoom(state.roomId) ?: return null
return MessagesViewModel(client, room, state) return MessagesViewModel(client, room, room.timeline(), state)
} }
} }
@ -39,7 +43,18 @@ class MessagesViewModel(
handleInit() handleInit()
} }
fun loadMore(){
viewModelScope.launch {
timeline.paginateBackwards(PAGINATION_COUNT)
setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) }
}
}
private fun handleInit() { private fun handleInit() {
setState {
copy(hasMoreToLoad = timeline.hasMoreToLoad)
}
room.syncUpdateFlow() room.syncUpdateFlow()
.onEach { .onEach {
val avatarData = val avatarData =
@ -51,7 +66,7 @@ class MessagesViewModel(
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
room.timeline().timelineItems() timeline.timelineItems()
.execute { .execute {
copy(timelineItems = it) copy(timelineItems = it)
} }

View file

@ -10,10 +10,15 @@ data class MessagesViewState(
val roomId: String, val roomId: String,
val roomName: String? = null, val roomName: String? = null,
val roomAvatar: AvatarData? = null, val roomAvatar: AvatarData? = null,
val timelineItems: Async<List<MatrixTimelineItem>> = Uninitialized val timelineItems: Async<List<MatrixTimelineItem>> = Uninitialized,
val hasMoreToLoad: Boolean = false,
) : MavericksState { ) : MavericksState {
@Suppress("unused") @Suppress("unused")
constructor(roomId: String) : this(roomId = roomId, roomName = null, roomAvatar = null) constructor(roomId: String) : this(
roomId = roomId,
roomName = null,
roomAvatar = null
)
} }

View file

@ -0,0 +1,2 @@
package io.element.android.x.core.data.flow

View file

@ -3,10 +3,13 @@ package io.element.android.x.matrix.room
import io.element.android.x.core.data.CoroutineDispatchers import io.element.android.x.core.data.CoroutineDispatchers
import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimeline
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.map
import org.matrix.rustcomponents.sdk.* import kotlinx.coroutines.flow.onStart
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.UpdateSummary
class MatrixRoom( class MatrixRoom(
private val slidingSyncUpdateFlow: Flow<UpdateSummary>, private val slidingSyncUpdateFlow: Flow<UpdateSummary>,
@ -15,7 +18,6 @@ class MatrixRoom(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
fun syncUpdateFlow(): Flow<Unit> { fun syncUpdateFlow(): Flow<Unit> {
return slidingSyncUpdateFlow return slidingSyncUpdateFlow
.filter { .filter {
@ -26,11 +28,7 @@ class MatrixRoom(
} }
fun timeline(): MatrixTimeline { fun timeline(): MatrixTimeline {
return MatrixTimeline(this) return MatrixTimeline(this, room, coroutineDispatchers)
}
internal fun timelineDiff(): Flow<TimelineDiff> {
return room.timelineDiff()
} }
val roomId = RoomId(room.id()) val roomId = RoomId(room.id())
@ -55,18 +53,5 @@ class MatrixRoom(
return room.avatarUrl() return room.avatarUrl()
} }
fun addTimelineListener(timelineListener: TimelineListener) {
room.addTimelineListener(timelineListener)
}
suspend fun paginateBackwards(count: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
if (!paginationOutcome.value.moreMessages) {
return@withContext Result.failure(IllegalStateException("no more message"))
}
runCatching {
paginationOutcome.value = room.paginateBackwards(count.toUShort())
}
}
} }

View file

@ -1,17 +1,20 @@
package io.element.android.x.matrix.timeline package io.element.android.x.matrix.timeline
import io.element.android.x.core.data.CoroutineDispatchers
import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.room.timelineDiff
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.matrix.rustcomponents.sdk.TimelineChange import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.*
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
class MatrixTimeline( class MatrixTimeline(
private val room: MatrixRoom, private val matrixRoom: MatrixRoom,
private val room: Room,
private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
interface Callback { interface Callback {
fun onUpdatedTimelineItem(eventId: EventId) fun onUpdatedTimelineItem(eventId: EventId)
fun onStartedBackPaginating() fun onStartedBackPaginating()
@ -20,6 +23,7 @@ class MatrixTimeline(
var callback: Callback? = null var callback: Callback? = null
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList()) MutableStateFlow(emptyList())
@ -30,6 +34,12 @@ class MatrixTimeline(
} }
} }
val hasMoreToLoad: Boolean
get() {
return paginationOutcome.value.moreMessages
}
private fun diffFlow(): Flow<Unit> { private fun diffFlow(): Flow<Unit> {
return room.timelineDiff() return room.timelineDiff()
.onEach { timelineDiff -> .onEach { timelineDiff ->
@ -78,28 +88,31 @@ class MatrixTimeline(
} }
} }
suspend fun paginateBackwards(count: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
if (!paginationOutcome.value.moreMessages) {
return@withContext Result.failure(IllegalStateException("no more message"))
}
runCatching {
paginationOutcome.value = room.paginateBackwards(count.toUShort())
}
}
private fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) { private fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) {
val mutableTimelineItems = timelineItems.value.toMutableList() val mutableTimelineItems = timelineItems.value.toMutableList()
block(mutableTimelineItems) block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems timelineItems.value = mutableTimelineItems
} }
fun addListener(timelineListener: TimelineListener) {
suspend fun processItemAppearance(itemId: String) { room.addTimelineListener(timelineListener)
} }
suspend fun processItemDisappearance(itemId: String) { fun dispose(){
room.removeTimeline()
}
suspend fun paginateBackwards(count: Int): Result<Unit> {
return room.paginateBackwards(count)
} }
suspend fun sendMessage(message: String): Result<Unit> { suspend fun sendMessage(message: String): Result<Unit> {
return Result.success(Unit) return Result.success(Unit)
} }
} }