Quickly branch pagination
This commit is contained in:
parent
da8b307339
commit
bff815e2c5
6 changed files with 91 additions and 47 deletions
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
package io.element.android.x.core.data.flow
|
||||||
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue