diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index 1ed45199be..f08529a2bd 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -8,11 +8,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn 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.material.ripple.rememberRipple import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -33,7 +34,14 @@ fun MessagesScreen(roomId: String) { val roomTitle by viewModel.collectAsState(MessagesViewState::roomName) val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar) 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 @@ -41,6 +49,8 @@ fun MessagesContent( roomTitle: String?, roomAvatar: AvatarData?, timelineItems: List, + hasMoreToLoad: Boolean, + onReachedLoadMore: () -> Unit, ) { LogCompositions(tag = "MessagesScreen", msg = "Content") val lazyListState = rememberLazyListState() @@ -61,7 +71,9 @@ fun MessagesContent( TimelineItems( padding = padding, lazyListState = lazyListState, - timelineItems = timelineItems + timelineItems = timelineItems, + hasMoreToLoad = hasMoreToLoad, + onReachedLoadMore = onReachedLoadMore, ) } ) @@ -71,7 +83,9 @@ fun MessagesContent( fun TimelineItems( padding: PaddingValues, lazyListState: LazyListState, - timelineItems: List + timelineItems: List, + hasMoreToLoad: Boolean, + onReachedLoadMore: () -> Unit, ) { LazyColumn( modifier = Modifier @@ -82,12 +96,18 @@ fun TimelineItems( verticalArrangement = Arrangement.Bottom, reverseLayout = true ) { - items(timelineItems) { timelineItem -> + itemsIndexed(timelineItems) { index, timelineItem -> TimelineItemRow(timelineItem = timelineItem) } + if (hasMoreToLoad) { + item { + MessagesLoadingMoreIndicator(onReachedLoadMore) + } + } } } + @Composable fun TimelineItemRow( timelineItem: MatrixTimelineItem @@ -124,7 +144,7 @@ fun TimelineItemRow( } @Composable -internal fun MessagesLoadingMoreIndicator() { +internal fun MessagesLoadingMoreIndicator(onReachedLoadMore: () -> Unit) { Box( Modifier .fillMaxWidth() @@ -133,6 +153,10 @@ internal fun MessagesLoadingMoreIndicator() { contentAlignment = Alignment.Center, ) { CircularProgressIndicator(strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary) + LaunchedEffect(Unit) { + onReachedLoadMore() + } } + } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index ab7d2aab5a..be37e5326f 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -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.MatrixInstance 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.onEach +import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.mediaSourceFromUrl +private const val PAGINATION_COUNT = 50 class MessagesViewModel( private val client: MatrixClient, private val room: MatrixRoom, + private val timeline: MatrixTimeline, private val initialState: MessagesViewState ) : MavericksViewModel(initialState) { @@ -29,7 +33,7 @@ class MessagesViewModel( val matrix = MatrixInstance.getInstance() val client = matrix.activeClient() 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() } + fun loadMore(){ + viewModelScope.launch { + timeline.paginateBackwards(PAGINATION_COUNT) + setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) } + } + } + private fun handleInit() { + setState { + copy(hasMoreToLoad = timeline.hasMoreToLoad) + } + room.syncUpdateFlow() .onEach { val avatarData = @@ -51,7 +66,7 @@ class MessagesViewModel( } }.launchIn(viewModelScope) - room.timeline().timelineItems() + timeline.timelineItems() .execute { copy(timelineItems = it) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt index e2c110b207..b739011eec 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt @@ -10,10 +10,15 @@ data class MessagesViewState( val roomId: String, val roomName: String? = null, val roomAvatar: AvatarData? = null, - val timelineItems: Async> = Uninitialized + val timelineItems: Async> = Uninitialized, + val hasMoreToLoad: Boolean = false, ) : MavericksState { @Suppress("unused") - constructor(roomId: String) : this(roomId = roomId, roomName = null, roomAvatar = null) + constructor(roomId: String) : this( + roomId = roomId, + roomName = null, + roomAvatar = null + ) } diff --git a/libraries/core/src/main/java/io/element/android/x/core/data/flow/TimingOperators.kt b/libraries/core/src/main/java/io/element/android/x/core/data/flow/TimingOperators.kt new file mode 100644 index 0000000000..cfa9f146c1 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/data/flow/TimingOperators.kt @@ -0,0 +1,2 @@ +package io.element.android.x.core.data.flow + diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index b5f0736be9..a6bbca534f 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -3,10 +3,13 @@ package io.element.android.x.matrix.room import io.element.android.x.core.data.CoroutineDispatchers import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.timeline.MatrixTimeline -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +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( private val slidingSyncUpdateFlow: Flow, @@ -15,7 +18,6 @@ class MatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) { - private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { @@ -26,11 +28,7 @@ class MatrixRoom( } fun timeline(): MatrixTimeline { - return MatrixTimeline(this) - } - - internal fun timelineDiff(): Flow { - return room.timelineDiff() + return MatrixTimeline(this, room, coroutineDispatchers) } val roomId = RoomId(room.id()) @@ -55,18 +53,5 @@ class MatrixRoom( return room.avatarUrl() } - fun addTimelineListener(timelineListener: TimelineListener) { - room.addTimelineListener(timelineListener) - } - - suspend fun paginateBackwards(count: Int): Result = withContext(coroutineDispatchers.io) { - if (!paginationOutcome.value.moreMessages) { - return@withContext Result.failure(IllegalStateException("no more message")) - } - runCatching { - paginationOutcome.value = room.paginateBackwards(count.toUShort()) - } - } - } \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index db26e2780e..6a6dceb30f 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -1,17 +1,20 @@ 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.room.MatrixRoom +import io.element.android.x.matrix.room.timelineDiff import kotlinx.coroutines.flow.* -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.* import timber.log.Timber import java.util.* class MatrixTimeline( - private val room: MatrixRoom, + private val matrixRoom: MatrixRoom, + private val room: Room, + private val coroutineDispatchers: CoroutineDispatchers, ) { - interface Callback { fun onUpdatedTimelineItem(eventId: EventId) fun onStartedBackPaginating() @@ -20,6 +23,7 @@ class MatrixTimeline( var callback: Callback? = null + private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) private val timelineItems: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -30,6 +34,12 @@ class MatrixTimeline( } } + val hasMoreToLoad: Boolean + get() { + return paginationOutcome.value.moreMessages + } + + private fun diffFlow(): Flow { return room.timelineDiff() .onEach { timelineDiff -> @@ -78,28 +88,31 @@ class MatrixTimeline( } } + suspend fun paginateBackwards(count: Int): Result = 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.() -> Unit) { val mutableTimelineItems = timelineItems.value.toMutableList() block(mutableTimelineItems) timelineItems.value = mutableTimelineItems } - - suspend fun processItemAppearance(itemId: String) { - + fun addListener(timelineListener: TimelineListener) { + room.addTimelineListener(timelineListener) } - suspend fun processItemDisappearance(itemId: String) { - - } - - suspend fun paginateBackwards(count: Int): Result { - return room.paginateBackwards(count) + fun dispose(){ + room.removeTimeline() } suspend fun sendMessage(message: String): Result { return Result.success(Unit) } - } \ No newline at end of file