Start implementing timeline

This commit is contained in:
ganfra 2022-11-07 12:36:03 +01:00
parent ed764c097f
commit 8f3233e450
9 changed files with 289 additions and 23 deletions

View file

@ -3,18 +3,28 @@
package io.element.android.x.features.messages
import Avatar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.clickable
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.rememberLazyListState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.data.LogCompositions
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.matrix.timeline.MatrixTimelineItem
@Composable
fun MessagesScreen(roomId: String) {
@ -22,19 +32,19 @@ fun MessagesScreen(roomId: String) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
val roomTitle by viewModel.collectAsState(MessagesViewState::roomName)
val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar)
MessagesContent(roomTitle, roomAvatar)
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
MessagesContent(roomTitle, roomAvatar, timelineItems().orEmpty())
}
@Composable
fun MessagesContent(
roomTitle: String?,
roomAvatar: AvatarData?
roomAvatar: AvatarData?,
timelineItems: List<MatrixTimelineItem>,
) {
val appBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
LogCompositions(tag = "RoomListScreen", msg = "Content")
LogCompositions(tag = "MessagesScreen", msg = "Content")
val lazyListState = rememberLazyListState()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
navigationIcon = {
@ -48,10 +58,81 @@ fun MessagesContent(
)
},
content = { padding ->
Box(modifier = Modifier.padding(padding))
TimelineItems(
padding = padding,
lazyListState = lazyListState,
timelineItems = timelineItems
)
}
)
}
@Composable
fun TimelineItems(
padding: PaddingValues,
lazyListState: LazyListState,
timelineItems: List<MatrixTimelineItem>
) {
LazyColumn(
modifier = Modifier
.padding(padding)
.fillMaxSize(),
state = lazyListState,
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Bottom,
reverseLayout = true
) {
items(timelineItems) { timelineItem ->
TimelineItemRow(timelineItem = timelineItem)
}
}
}
@Composable
fun TimelineItemRow(
timelineItem: MatrixTimelineItem
) {
when (timelineItem) {
MatrixTimelineItem.Other -> return
MatrixTimelineItem.Virtual -> return
is MatrixTimelineItem.Event -> {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically
) {
Text(
fontSize = 14.sp,
text = timelineItem.event.raw() ?: "",
)
}
}
}
}
}
@Composable
internal fun MessagesLoadingMoreIndicator() {
Box(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary)
}
}

View file

@ -50,6 +50,11 @@ class MessagesViewModel(
)
}
}.launchIn(viewModelScope)
room.timeline().timelineItems()
.execute {
copy(timelineItems = it)
}
}
private suspend fun loadAvatarData(

View file

@ -1,12 +1,16 @@
package io.element.android.x.features.messages.model
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.matrix.timeline.MatrixTimelineItem
data class MessagesViewState(
val roomId: String,
val roomName: String? = null,
val roomAvatar: AvatarData? = null
val roomAvatar: AvatarData? = null,
val timelineItems: Async<List<MatrixTimelineItem>> = Uninitialized
) : MavericksState {
@Suppress("unused")

View file

@ -4,9 +4,7 @@ import io.element.android.x.core.data.CoroutineDispatchers
import io.element.android.x.matrix.core.UserId
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.room.RoomSummaryDataSource
import io.element.android.x.matrix.room.RoomSummaryDetailsFactory
import io.element.android.x.matrix.room.RustRoomSummaryDataSource
import io.element.android.x.matrix.room.message.RoomMessageFactory
import io.element.android.x.matrix.session.SessionStore
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy
import kotlinx.coroutines.CoroutineScope
@ -58,7 +56,12 @@ class MatrixClient internal constructor(
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
private val roomSummaryDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(slidingSyncObserverProxy.updateSummaryFlow, slidingSync, slidingSyncView, dispatchers)
RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow,
slidingSync,
slidingSyncView,
dispatchers
)
private var slidingSyncObserverToken: StoppableSpawn? = null
init {
@ -71,7 +74,8 @@ class MatrixClient internal constructor(
return MatrixRoom(
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
slidingSyncRoom = slidingSyncRoom,
room = room
room = room,
coroutineDispatchers = dispatchers
)
}

View file

@ -1,20 +1,21 @@
package io.element.android.x.matrix.room
import io.element.android.x.core.data.CoroutineDispatchers
import io.element.android.x.matrix.core.RoomId
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
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.*
class MatrixRoom(
private val slidingSyncUpdateFlow: Flow<UpdateSummary>,
private val slidingSyncRoom: SlidingSyncRoom,
private val room: Room,
private val coroutineDispatchers: CoroutineDispatchers,
) {
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
fun syncUpdateFlow(): Flow<Unit> {
return slidingSyncUpdateFlow
.filter {
@ -24,6 +25,14 @@ class MatrixRoom(
.onStart { emit(Unit) }
}
fun timeline(): MatrixTimeline {
return MatrixTimeline(this)
}
internal fun timelineDiff(): Flow<TimelineDiff> {
return room.timelineDiff()
}
val roomId = RoomId(room.id())
val name: String?
@ -46,5 +55,18 @@ class MatrixRoom(
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

@ -0,0 +1,23 @@
package io.element.android.x.matrix.room
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
fun Room.timelineDiff(): Flow<TimelineDiff> = callbackFlow {
val listener = object : TimelineListener {
override fun onUpdate(update: TimelineDiff) {
trySend(update)
}
}
addTimelineListener(listener)
awaitClose {
removeTimeline()
}
}

View file

@ -95,7 +95,7 @@ internal class RustRoomSummaryDataSource(
add(roomSummary)
}
is SlidingSyncViewRoomsListDiff.UpdateAt -> {
//fillUntil(diff.index.toInt())
fillUntil(diff.index.toInt())
val roomSummary = buildSummaryForRoomListEntry(diff.value)
set(diff.index.toInt(), roomSummary)
}

View file

@ -0,0 +1,105 @@
package io.element.android.x.matrix.timeline
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom
import kotlinx.coroutines.flow.*
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import timber.log.Timber
import java.util.*
class MatrixTimeline(
private val room: MatrixRoom,
) {
interface Callback {
fun onUpdatedTimelineItem(eventId: EventId)
fun onStartedBackPaginating()
fun onFinishedBackPaginating()
}
var callback: Callback? = null
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return diffFlow().combine(timelineItems) { _, _ ->
timelineItems.value.reversed()
}
}
private fun diffFlow(): Flow<Unit> {
return room.timelineDiff()
.onEach { timelineDiff ->
updateTimelineItems {
applyDiff(timelineDiff)
}
}.map { }
}
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
Timber.v("ApplyDiff: ${diff.change()} for list with size: $size")
when (diff.change()) {
TimelineChange.PUSH -> {
val item = diff.push()?.asMatrixTimelineItem() ?: return
add(item)
}
TimelineChange.UPDATE_AT -> {
val updateAtData = diff.updateAt() ?: return
val item = updateAtData.item.asMatrixTimelineItem()
set(updateAtData.index.toInt(), item)
}
TimelineChange.INSERT_AT -> {
val insertAtData = diff.insertAt() ?: return
val item = insertAtData.item.asMatrixTimelineItem()
add(insertAtData.index.toInt(), item)
}
TimelineChange.MOVE -> {
val moveData = diff.move() ?: return
Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt())
}
TimelineChange.REMOVE_AT -> {
val removeAtData = diff.removeAt() ?: return
removeAt(removeAtData.toInt())
}
TimelineChange.REPLACE -> {
clear()
val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return
addAll(items)
}
TimelineChange.POP -> {
removeLast()
}
TimelineChange.CLEAR -> {
clear()
}
}
}
private fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) {
val mutableTimelineItems = timelineItems.value.toMutableList()
block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems
}
suspend fun processItemAppearance(itemId: String) {
}
suspend fun processItemDisappearance(itemId: String) {
}
suspend fun paginateBackwards(count: Int): Result<Unit> {
return room.paginateBackwards(count)
}
suspend fun sendMessage(message: String): Result<Unit> {
return Result.success(Unit)
}
}

View file

@ -0,0 +1,22 @@
package io.element.android.x.matrix.timeline
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.TimelineItem
sealed interface MatrixTimelineItem {
data class Event(val event: EventTimelineItem) : MatrixTimelineItem
object Virtual : MatrixTimelineItem
object Other : MatrixTimelineItem
}
fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem {
val asEvent = asEvent()
if (asEvent != null) {
return MatrixTimelineItem.Event(asEvent)
}
val asVirtual = asVirtual()
if (asVirtual != null) {
return MatrixTimelineItem.Virtual
}
return MatrixTimelineItem.Other
}