Start implementing timeline
This commit is contained in:
parent
f14a02a68e
commit
da8b307339
9 changed files with 289 additions and 23 deletions
|
|
@ -3,18 +3,28 @@
|
||||||
package io.element.android.x.features.messages
|
package io.element.android.x.features.messages
|
||||||
|
|
||||||
import Avatar
|
import Avatar
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.padding
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.collectAsState
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||||
import io.element.android.x.core.data.LogCompositions
|
import io.element.android.x.core.data.LogCompositions
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.features.messages.model.MessagesViewState
|
import io.element.android.x.features.messages.model.MessagesViewState
|
||||||
|
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesScreen(roomId: String) {
|
fun MessagesScreen(roomId: String) {
|
||||||
|
|
@ -22,19 +32,19 @@ fun MessagesScreen(roomId: String) {
|
||||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||||
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)
|
||||||
MessagesContent(roomTitle, roomAvatar)
|
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
|
||||||
|
MessagesContent(roomTitle, roomAvatar, timelineItems().orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesContent(
|
fun MessagesContent(
|
||||||
roomTitle: String?,
|
roomTitle: String?,
|
||||||
roomAvatar: AvatarData?
|
roomAvatar: AvatarData?,
|
||||||
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
) {
|
) {
|
||||||
val appBarState = rememberTopAppBarState()
|
LogCompositions(tag = "MessagesScreen", msg = "Content")
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
val lazyListState = rememberLazyListState()
|
||||||
LogCompositions(tag = "RoomListScreen", msg = "Content")
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -48,10 +58,81 @@ fun MessagesContent(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { padding ->
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@ class MessagesViewModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
room.timeline().timelineItems()
|
||||||
|
.execute {
|
||||||
|
copy(timelineItems = it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadAvatarData(
|
private suspend fun loadAvatarData(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package io.element.android.x.features.messages.model
|
package io.element.android.x.features.messages.model
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||||
|
|
||||||
data class MessagesViewState(
|
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
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
|
||||||
|
|
@ -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.core.UserId
|
||||||
import io.element.android.x.matrix.room.MatrixRoom
|
import io.element.android.x.matrix.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
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.RustRoomSummaryDataSource
|
||||||
import io.element.android.x.matrix.room.message.RoomMessageFactory
|
|
||||||
import io.element.android.x.matrix.session.SessionStore
|
import io.element.android.x.matrix.session.SessionStore
|
||||||
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy
|
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -58,7 +56,12 @@ class MatrixClient internal constructor(
|
||||||
|
|
||||||
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
|
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
|
||||||
private val roomSummaryDataSource: RustRoomSummaryDataSource =
|
private val roomSummaryDataSource: RustRoomSummaryDataSource =
|
||||||
RustRoomSummaryDataSource(slidingSyncObserverProxy.updateSummaryFlow, slidingSync, slidingSyncView, dispatchers)
|
RustRoomSummaryDataSource(
|
||||||
|
slidingSyncObserverProxy.updateSummaryFlow,
|
||||||
|
slidingSync,
|
||||||
|
slidingSyncView,
|
||||||
|
dispatchers
|
||||||
|
)
|
||||||
private var slidingSyncObserverToken: StoppableSpawn? = null
|
private var slidingSyncObserverToken: StoppableSpawn? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -71,7 +74,8 @@ class MatrixClient internal constructor(
|
||||||
return MatrixRoom(
|
return MatrixRoom(
|
||||||
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
|
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
|
||||||
slidingSyncRoom = slidingSyncRoom,
|
slidingSyncRoom = slidingSyncRoom,
|
||||||
room = room
|
room = room,
|
||||||
|
coroutineDispatchers = dispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
package io.element.android.x.matrix.room
|
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.core.RoomId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.*
|
||||||
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>,
|
||||||
private val slidingSyncRoom: SlidingSyncRoom,
|
private val slidingSyncRoom: SlidingSyncRoom,
|
||||||
private val room: Room,
|
private val room: Room,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
|
||||||
fun syncUpdateFlow(): Flow<Unit> {
|
fun syncUpdateFlow(): Flow<Unit> {
|
||||||
return slidingSyncUpdateFlow
|
return slidingSyncUpdateFlow
|
||||||
.filter {
|
.filter {
|
||||||
|
|
@ -24,6 +25,14 @@ class MatrixRoom(
|
||||||
.onStart { emit(Unit) }
|
.onStart { emit(Unit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun timeline(): MatrixTimeline {
|
||||||
|
return MatrixTimeline(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun timelineDiff(): Flow<TimelineDiff> {
|
||||||
|
return room.timelineDiff()
|
||||||
|
}
|
||||||
|
|
||||||
val roomId = RoomId(room.id())
|
val roomId = RoomId(room.id())
|
||||||
|
|
||||||
val name: String?
|
val name: String?
|
||||||
|
|
@ -46,5 +55,18 @@ 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ internal class RustRoomSummaryDataSource(
|
||||||
add(roomSummary)
|
add(roomSummary)
|
||||||
}
|
}
|
||||||
is SlidingSyncViewRoomsListDiff.UpdateAt -> {
|
is SlidingSyncViewRoomsListDiff.UpdateAt -> {
|
||||||
//fillUntil(diff.index.toInt())
|
fillUntil(diff.index.toInt())
|
||||||
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
||||||
set(diff.index.toInt(), roomSummary)
|
set(diff.index.toInt(), roomSummary)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue