Start integrating uptodate rust sdk and make it compile (WIP)

This commit is contained in:
ganfra 2023-02-03 10:35:30 +01:00
parent 95b8cb6c03
commit f0db8b00ea
34 changed files with 739 additions and 352 deletions

View file

@ -20,5 +20,5 @@ import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface MessagesEvents { sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
} }

View file

@ -78,7 +78,7 @@ class MessagesPresenter @Inject constructor(
} }
fun handleEvents(event: MessagesEvents) { fun handleEvents(event: MessagesEvents) {
when (event) { when (event) {
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState) is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
} }
} }
return MessagesState( return MessagesState(
@ -94,7 +94,7 @@ class MessagesPresenter @Inject constructor(
fun CoroutineScope.handleTimelineAction( fun CoroutineScope.handleTimelineAction(
action: TimelineItemAction, action: TimelineItemAction,
targetEvent: TimelineItem.MessageEvent, targetEvent: TimelineItem.Event,
composerState: MessageComposerState, composerState: MessageComposerState,
) = launch { ) = launch {
when (action) { when (action) {
@ -110,11 +110,11 @@ class MessagesPresenter @Inject constructor(
Timber.v("NotImplementedYet") Timber.v("NotImplementedYet")
} }
private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) { private suspend fun handleActionRedact(event: TimelineItem.Event) {
room.redactEvent(event.id) room.redactEvent(event.id)
} }
private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) { private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Edit( val composerMode = MessageComposerMode.Edit(
targetEvent.id, targetEvent.id,
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty() (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty()
@ -124,7 +124,7 @@ class MessagesPresenter @Inject constructor(
) )
} }
private fun handleActionReply(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) { private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "") val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "")
composerState.eventSink( composerState.eventSink(
MessageComposerEvents.SetMode(composerMode) MessageComposerEvents.SetMode(composerMode)

View file

@ -84,21 +84,21 @@ fun MessagesView(
LogCompositions(tag = "MessagesScreen", msg = "Content") LogCompositions(tag = "MessagesScreen", msg = "Content")
fun onMessageClicked(messageEvent: TimelineItem.MessageEvent) { fun onMessageClicked(event: TimelineItem.Event) {
Timber.v("OnMessageClicked= ${messageEvent.id}") Timber.v("OnMessageClicked= ${event.id}")
} }
fun onMessageLongClicked(messageEvent: TimelineItem.MessageEvent) { fun onMessageLongClicked(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${messageEvent.id}") Timber.v("OnMessageLongClicked= ${event.id}")
focusManager.clearFocus(force = true) focusManager.clearFocus(force = true)
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(messageEvent)) state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
coroutineScope.launch { coroutineScope.launch {
itemActionsBottomSheetState.show() itemActionsBottomSheetState.show()
} }
} }
fun onActionSelected(action: TimelineItemAction, messageEvent: TimelineItem.MessageEvent) { fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) state.eventSink(MessagesEvents.HandleAction(action, event))
} }
Scaffold( Scaffold(
@ -138,8 +138,8 @@ fun MessagesView(
fun MessagesViewContent( fun MessagesViewContent(
state: MessagesState, state: MessagesState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {}, onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) { ) {
Column( Column(
modifier = modifier modifier = modifier

View file

@ -20,5 +20,5 @@ import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface ActionListEvents { sealed interface ActionListEvents {
object Clear : ActionListEvents object Clear : ActionListEvents
data class ComputeForMessage(val messageEvent: TimelineItem.MessageEvent) : ActionListEvents data class ComputeForMessage(val event: TimelineItem.Event) : ActionListEvents
} }

View file

@ -43,7 +43,7 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
fun handleEvents(event: ActionListEvents) { fun handleEvents(event: ActionListEvents) {
when (event) { when (event) {
ActionListEvents.Clear -> target.value = ActionListState.Target.None ActionListEvents.Clear -> target.value = ActionListState.Target.None
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(event.messageEvent, target) is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(event.event, target)
} }
} }
@ -53,7 +53,7 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
) )
} }
fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.MessageEvent, target: MutableState<ActionListState.Target>) = launch { fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
target.value = ActionListState.Target.Loading(timelineItem) target.value = ActionListState.Target.Loading(timelineItem)
val actions = val actions =
if (timelineItem.content is TimelineItemRedactedContent) { if (timelineItem.content is TimelineItemRedactedContent) {

View file

@ -29,9 +29,9 @@ data class ActionListState(
sealed interface Target { sealed interface Target {
object None : Target object None : Target
data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target data class Loading(val event: TimelineItem.Event) : Target
data class Success( data class Success(
val messageEvent: TimelineItem.MessageEvent, val event: TimelineItem.Event,
val actions: ImmutableList<TimelineItemAction>, val actions: ImmutableList<TimelineItemAction>,
) : Target ) : Target
} }

View file

@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
fun ActionListView( fun ActionListView(
state: ActionListState, state: ActionListState,
modalBottomSheetState: ModalBottomSheetState, modalBottomSheetState: ModalBottomSheetState,
onActionSelected: (action: TimelineItemAction, TimelineItem.MessageEvent) -> Unit, onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -65,7 +65,7 @@ fun ActionListView(
fun onItemActionClicked( fun onItemActionClicked(
itemAction: TimelineItemAction, itemAction: TimelineItemAction,
targetItem: TimelineItem.MessageEvent targetItem: TimelineItem.Event
) { ) {
onActionSelected(itemAction, targetItem) onActionSelected(itemAction, targetItem)
coroutineScope.launch { coroutineScope.launch {
@ -92,7 +92,7 @@ fun ActionListView(
private fun SheetContent( private fun SheetContent(
state: ActionListState, state: ActionListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onActionClicked: (TimelineItemAction, TimelineItem.MessageEvent) -> Unit = { _, _ -> }, onActionClicked: (TimelineItemAction, TimelineItem.Event) -> Unit = { _, _ -> },
) { ) {
when (val target = state.target) { when (val target = state.target) {
is ActionListState.Target.Loading, is ActionListState.Target.Loading,
@ -110,7 +110,7 @@ private fun SheetContent(
) { action -> ) { action ->
ListItem( ListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
onActionClicked(action, target.messageEvent) onActionClicked(action, target.event)
}, },
text = { text = {
Text( Text(

View file

@ -1,252 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline
import androidx.recyclerview.widget.DiffUtil
import io.element.android.features.messages.timeline.diff.CacheInvalidator
import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEmoteContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemNoticeContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.util.invalidateLast
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.MessageType
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis
class TimelineItemsFactory @Inject constructor(
private val matrixItemHelper: MatrixItemHelper,
private val room: MatrixRoom,
private val dispatcher: CoroutineDispatcher,
) {
private val timelineItems = MutableStateFlow<List<TimelineItem>>(emptyList())
private val timelineItemsCache = arrayListOf<TimelineItem?>()
// Items from rust sdk, used for diffing
private var matrixTimelineItems: List<MatrixTimelineItem> = emptyList()
private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
fun flow(): StateFlow<List<TimelineItem>> = timelineItems.asStateFlow()
suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>,
) = withContext(dispatcher) {
lock.withLock {
calculateAndApplyDiff(timelineItems)
buildAndEmitTimelineItemStates(timelineItems)
}
}
suspend fun pushItem(
timelineItem: MatrixTimelineItem,
) = withContext(dispatcher) {
lock.withLock {
// Makes sure to invalidate last as we need to recompute some data (like groupPosition)
timelineItemsCache.invalidateLast()
timelineItemsCache.add(null)
matrixTimelineItems = matrixTimelineItems + timelineItem
buildAndEmitTimelineItemStates(matrixTimelineItems)
}
}
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
val newTimelineItemStates = ArrayList<TimelineItem>()
for (index in timelineItemsCache.indices.reversed()) {
val cacheItem = timelineItemsCache[index]
if (cacheItem == null) {
buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
newTimelineItemStates.add(timelineItemState)
}
} else {
newTimelineItemStates.add(cacheItem)
}
}
this.timelineItems.emit(newTimelineItemStates)
}
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
val timeToDiff = measureTimeMillis {
val diffCallback =
MatrixTimelineItemsDiffCallback(
oldList = matrixTimelineItems,
newList = newTimelineItems
)
val diffResult = DiffUtil.calculateDiff(diffCallback, false)
matrixTimelineItems = newTimelineItems
diffResult.dispatchUpdatesTo(cacheInvalidator)
}
Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms")
}
private suspend fun buildAndCacheItem(
timelineItems: List<MatrixTimelineItem>,
index: Int
): TimelineItem? {
val timelineItemState =
when (val currentTimelineItem = timelineItems[index]) {
is MatrixTimelineItem.Event -> {
buildMessageEvent(
currentTimelineItem,
index,
timelineItems,
)
}
is MatrixTimelineItem.Virtual -> TimelineItem.Virtual(
"virtual_item_$index"
)
MatrixTimelineItem.Other -> null
}
timelineItemsCache[index] = timelineItemState
return timelineItemState
}
private suspend fun buildMessageEvent(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List<MatrixTimelineItem>,
): TimelineItem.MessageEvent {
val currentSender = currentTimelineItem.event.sender()
val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData = AvatarData(
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
)
return TimelineItem.MessageEvent(
id = EventId(currentTimelineItem.uniqueId),
senderId = currentSender,
senderDisplayName = senderDisplayName,
senderAvatar = senderAvatarData,
content = currentTimelineItem.computeContent(),
isMine = currentTimelineItem.event.isOwn(),
groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState()
)
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val aggregatedReactions = event.reactions().map {
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
}
return TimelineItemReactions(aggregatedReactions.toImmutableList())
}
private fun MatrixTimelineItem.Event.computeContent(): TimelineItemContent {
val content = event.content()
content.asUnableToDecrypt()?.let { encryptedMessage ->
return TimelineItemEncryptedContent(encryptedMessage)
}
if (content.isRedactedMessage()) {
return TimelineItemRedactedContent
}
val contentAsMessage = content.asMessage()
return when (val messageType = contentAsMessage?.msgtype()) {
is MessageType.Emote -> TimelineItemEmoteContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
is MessageType.Image -> {
val height = messageType.content.info?.height?.toFloat()
val width = messageType.content.info?.width?.toFloat()
val aspectRatio = if (height != null && width != null) {
width / height
} else {
0.7f
}
TimelineItemImageContent(
body = messageType.content.body,
imageMeta = MediaResolver.Meta(
source = messageType.content.source,
kind = MediaResolver.Kind.Content
),
blurhash = messageType.content.info?.blurhash,
aspectRatio = aspectRatio
)
}
is MessageType.Notice -> TimelineItemNoticeContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
is MessageType.Text -> TimelineItemTextContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
else -> TimelineItemUnknownContent
}
}
private fun FormattedBody.toHtmlDocument(): Document? {
return takeIf { it.format == MessageFormat.HTML }?.body?.let { formattedBody ->
Jsoup.parse(formattedBody)
}
}
private fun computeGroupPosition(
currentTimelineItem: MatrixTimelineItem.Event,
timelineItems: List<MatrixTimelineItem>,
index: Int
): MessagesItemGroupPosition {
val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem =
timelineItems.getOrNull(index + 1) as? MatrixTimelineItem.Event
val currentSender = currentTimelineItem.event.sender()
val previousSender = prevTimelineItem?.event?.sender()
val nextSender = nextTimelineItem?.event?.sender()
return when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
else -> MessagesItemGroupPosition.None
}
}
}

View file

@ -24,14 +24,12 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.messages.timeline.factories.TimelineItemsFactory
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -39,18 +37,15 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val PAGINATION_COUNT = 50 private const val backPaginationEventLimit = 20
private const val backPaginationPageSize = 50
class TimelinePresenter @Inject constructor( class TimelinePresenter @Inject constructor(
coroutineDispatchers: CoroutineDispatchers, private val timelineItemsFactory: TimelineItemsFactory,
client: MatrixClient,
room: MatrixRoom, room: MatrixRoom,
) : Presenter<TimelineState> { ) : Presenter<TimelineState> {
private val timeline = room.timeline() private val timeline = room.timeline()
private val matrixItemHelper = MatrixItemHelper(client)
private val timelineItemsFactory =
TimelineItemsFactory(matrixItemHelper, room, coroutineDispatchers.computation)
private class TimelineCallback( private class TimelineCallback(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
@ -66,9 +61,6 @@ class TimelinePresenter @Inject constructor(
@Composable @Composable
override fun present(): TimelineState { override fun present(): TimelineState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val hasMoreToLoad = rememberSaveable {
mutableStateOf(timeline.hasMoreToLoad)
}
val highlightedEventId: MutableState<EventId?> = rememberSaveable { val highlightedEventId: MutableState<EventId?> = rememberSaveable {
mutableStateOf(null) mutableStateOf(null)
} }
@ -78,7 +70,7 @@ class TimelinePresenter @Inject constructor(
fun handleEvents(event: TimelineEvents) { fun handleEvents(event: TimelineEvents) {
when (event) { when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(hasMoreToLoad) TimelineEvents.LoadMore -> localCoroutineScope.loadMore()
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
} }
} }
@ -102,13 +94,11 @@ class TimelinePresenter @Inject constructor(
return TimelineState( return TimelineState(
highlightedEventId = highlightedEventId.value, highlightedEventId = highlightedEventId.value,
timelineItems = timelineItems.value.toImmutableList(), timelineItems = timelineItems.value.toImmutableList(),
hasMoreToLoad = hasMoreToLoad.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch { private fun CoroutineScope.loadMore() = launch {
timeline.paginateBackwards(PAGINATION_COUNT) timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
hasMoreToLoad.value = timeline.hasMoreToLoad
} }
} }

View file

@ -24,7 +24,6 @@ import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
data class TimelineState( data class TimelineState(
val timelineItems: ImmutableList<TimelineItem>, val timelineItems: ImmutableList<TimelineItem>,
val hasMoreToLoad: Boolean,
val highlightedEventId: EventId?, val highlightedEventId: EventId?,
val eventSink: (TimelineEvents) -> Unit val eventSink: (TimelineEvents) -> Unit
) )

View file

@ -90,8 +90,8 @@ import kotlinx.coroutines.launch
fun TimelineView( fun TimelineView(
state: TimelineState, state: TimelineState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {}, onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
Box(modifier = modifier) { Box(modifier = modifier) {
@ -114,11 +114,14 @@ fun TimelineView(
onLongClick = onMessageLongClicked onLongClick = onMessageLongClicked
) )
} }
/*
if (state.hasMoreToLoad) { if (state.hasMoreToLoad) {
item { item {
TimelineLoadingMoreIndicator() TimelineLoadingMoreIndicator()
} }
} }
*/
} }
fun onReachedLoadMore() { fun onReachedLoadMore() {
@ -135,14 +138,14 @@ fun TimelineView(
private fun TimelineItem.key(): String { private fun TimelineItem.key(): String {
return when (this) { return when (this) {
is TimelineItem.MessageEvent -> id.value is TimelineItem.Event -> id.value
is TimelineItem.Virtual -> id is TimelineItem.Virtual -> id
} }
} }
private fun TimelineItem.contentType(): Int { private fun TimelineItem.contentType(): Int {
return when (this) { return when (this) {
is TimelineItem.MessageEvent -> 0 is TimelineItem.Event -> 0
is TimelineItem.Virtual -> 1 is TimelineItem.Virtual -> 1
} }
} }
@ -151,13 +154,13 @@ private fun TimelineItem.contentType(): Int {
fun TimelineItemRow( fun TimelineItemRow(
timelineItem: TimelineItem, timelineItem: TimelineItem,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: (TimelineItem.MessageEvent) -> Unit, onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.MessageEvent) -> Unit, onLongClick: (TimelineItem.Event) -> Unit,
) { ) {
when (timelineItem) { when (timelineItem) {
is TimelineItem.Virtual -> return is TimelineItem.Virtual -> return
is TimelineItem.MessageEvent -> MessageEventRow( is TimelineItem.Event -> MessageEventRow(
messageEvent = timelineItem, event = timelineItem,
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
onClick = { onClick(timelineItem) }, onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) } onLongClick = { onLongClick(timelineItem) }
@ -167,14 +170,14 @@ fun TimelineItemRow(
@Composable @Composable
fun MessageEventRow( fun MessageEventRow(
messageEvent: TimelineItem.MessageEvent, event: TimelineItem.Event,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val (parentAlignment, contentAlignment) = if (messageEvent.isMine) { val (parentAlignment, contentAlignment) = if (event.isMine) {
Pair(Alignment.CenterEnd, Alignment.End) Pair(Alignment.CenterEnd, Alignment.End)
} else { } else {
Pair(Alignment.CenterStart, Alignment.Start) Pair(Alignment.CenterStart, Alignment.Start)
@ -186,20 +189,20 @@ fun MessageEventRow(
contentAlignment = parentAlignment contentAlignment = parentAlignment
) { ) {
Row { Row {
if (!messageEvent.isMine) { if (!event.isMine) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
} }
Column(horizontalAlignment = contentAlignment) { Column(horizontalAlignment = contentAlignment) {
if (messageEvent.showSenderInformation) { if (event.showSenderInformation) {
MessageSenderInformation( MessageSenderInformation(
messageEvent.safeSenderName, event.safeSenderName,
messageEvent.senderAvatar, event.senderAvatar,
Modifier.zIndex(1f) Modifier.zIndex(1f)
) )
} }
MessageEventBubble( MessageEventBubble(
groupPosition = messageEvent.groupPosition, groupPosition = event.groupPosition,
isMine = messageEvent.isMine, isMine = event.isMine,
interactionSource = interactionSource, interactionSource = interactionSource,
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
onClick = onClick, onClick = onClick,
@ -209,45 +212,45 @@ fun MessageEventRow(
.widthIn(max = 320.dp) .widthIn(max = 320.dp)
) { ) {
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
when (messageEvent.content) { when (event.content) {
is TimelineItemEncryptedContent -> TimelineItemEncryptedView( is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
content = messageEvent.content, content = event.content,
modifier = contentModifier modifier = contentModifier
) )
is TimelineItemRedactedContent -> TimelineItemRedactedView( is TimelineItemRedactedContent -> TimelineItemRedactedView(
content = messageEvent.content, content = event.content,
modifier = contentModifier modifier = contentModifier
) )
is TimelineItemTextBasedContent -> TimelineItemTextView( is TimelineItemTextBasedContent -> TimelineItemTextView(
content = messageEvent.content, content = event.content,
interactionSource = interactionSource, interactionSource = interactionSource,
modifier = contentModifier, modifier = contentModifier,
onTextClicked = onClick, onTextClicked = onClick,
onTextLongClicked = onLongClick onTextLongClicked = onLongClick
) )
is TimelineItemUnknownContent -> TimelineItemUnknownView( is TimelineItemUnknownContent -> TimelineItemUnknownView(
content = messageEvent.content, content = event.content,
modifier = contentModifier modifier = contentModifier
) )
is TimelineItemImageContent -> TimelineItemImageView( is TimelineItemImageContent -> TimelineItemImageView(
content = messageEvent.content, content = event.content,
modifier = contentModifier modifier = contentModifier
) )
} }
} }
TimelineItemReactionsView( TimelineItemReactionsView(
reactionsState = messageEvent.reactionsState, reactionsState = event.reactionsState,
modifier = Modifier modifier = Modifier
.zIndex(1f) .zIndex(1f)
.offset(x = if (messageEvent.isMine) 0.dp else 20.dp, y = -(16.dp)) .offset(x = if (event.isMine) 0.dp else 20.dp, y = -(16.dp))
) )
} }
if (messageEvent.isMine) { if (event.isMine) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
} }
} }
} }
if (messageEvent.groupPosition.isNew()) { if (event.groupPosition.isNew()) {
Spacer(modifier = modifier.height(8.dp)) Spacer(modifier = modifier.height(8.dp))
} else { } else {
Spacer(modifier = modifier.height(2.dp)) Spacer(modifier = modifier.height(2.dp))
@ -399,7 +402,6 @@ fun TimelineItemsPreview(
TimelineView( TimelineView(
state = TimelineState( state = TimelineState(
timelineItems = timelineItems, timelineItems = timelineItems,
hasMoreToLoad = true,
highlightedEventId = null, highlightedEventId = null,
eventSink = {} eventSink = {}
) )
@ -411,7 +413,7 @@ private fun createMessageEvent(
content: TimelineItemContent, content: TimelineItemContent,
groupPosition: MessagesItemGroupPosition groupPosition: MessagesItemGroupPosition
): TimelineItem { ): TimelineItem {
return TimelineItem.MessageEvent( return TimelineItem.Event(
id = EventId(Math.random().toString()), id = EventId(Math.random().toString()),
senderId = "senderId", senderId = "senderId",
senderAvatar = AvatarData("sender"), senderAvatar = AvatarData("sender"),

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
typealias RustTimelineItemContent = org.matrix.rustcomponents.sdk.TimelineItemContent
class TimelineItemContentFactory @Inject constructor(
private val messageFactory: TimelineItemContentMessageFactory,
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
private val stickerFactory: TimelineItemContentStickerFactory,
private val utdFactory: TimelineItemContentUTDFactory,
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
private val stateFactory: TimelineItemContentStateFactory,
private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory,
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
) {
fun create(itemContent: RustTimelineItemContent): TimelineItemContent {
return when (val kind = itemContent.kind()) {
is TimelineItemContentKind.Message -> messageFactory.create(itemContent.asMessage())
is TimelineItemContentKind.RedactedMessage -> redactedMessageFactory.create(kind)
is TimelineItemContentKind.Sticker -> stickerFactory.create(kind)
is TimelineItemContentKind.UnableToDecrypt -> utdFactory.create(kind)
is TimelineItemContentKind.RoomMembership -> roomMembershipFactory.create(kind)
is TimelineItemContentKind.ProfileChange -> profileChangeFactory.create(kind)
is TimelineItemContentKind.State -> stateFactory.create(kind)
is TimelineItemContentKind.FailedToParseMessageLike -> failedToParseMessageFactory.create(kind)
is TimelineItemContentKind.FailedToParseState -> failedToParseStateFactory.create(kind)
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentFailedToParseMessageFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.FailedToParseMessageLike): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentFailedToParseStateFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.FailedToParseState): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEmoteContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemNoticeContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.util.toHtmlDocument
import io.element.android.libraries.matrix.media.MediaResolver
import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType
import javax.inject.Inject
class TimelineItemContentMessageFactory @Inject constructor() {
fun create(contentAsMessage: Message?): TimelineItemContent {
return when (val messageType = contentAsMessage?.msgtype()) {
is MessageType.Emote -> TimelineItemEmoteContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
is MessageType.Image -> {
val height = messageType.content.info?.height?.toFloat()
val width = messageType.content.info?.width?.toFloat()
val aspectRatio = if (height != null && width != null) {
width / height
} else {
0.7f
}
TimelineItemImageContent(
body = messageType.content.body,
imageMeta = MediaResolver.Meta(
source = messageType.content.source,
kind = MediaResolver.Kind.Content
),
blurhash = messageType.content.info?.blurhash,
aspectRatio = aspectRatio
)
}
is MessageType.Notice -> TimelineItemNoticeContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
is MessageType.Text -> TimelineItemTextContent(
body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
else -> TimelineItemUnknownContent
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentProfileChangeFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.ProfileChange): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentRedactedFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.RedactedMessage): TimelineItemContent {
return TimelineItemRedactedContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentRoomMembershipFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.RoomMembership): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentStateFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.State): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentStickerFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.Sticker): TimelineItemContent {
return TimelineItemUnknownContent
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import javax.inject.Inject
class TimelineItemContentUTDFactory @Inject constructor() {
fun create(kind: TimelineItemContentKind.UnableToDecrypt): TimelineItemContent {
return TimelineItemEncryptedContent(kind.msg)
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
class TimelineItemEventFactory @Inject constructor(
private val room: MatrixRoom,
private val contentFactory: TimelineItemContentFactory,
) {
suspend fun create(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List<MatrixTimelineItem>,
): TimelineItem.Event {
val currentSender = currentTimelineItem.event.sender()
val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData = AvatarData(
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
)
return TimelineItem.Event(
id = EventId(currentTimelineItem.uniqueId),
senderId = currentSender,
senderDisplayName = senderDisplayName,
senderAvatar = senderAvatarData,
content = contentFactory.create(currentTimelineItem.event.content()),
isMine = currentTimelineItem.event.isOwn(),
groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState()
)
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val aggregatedReactions = event.reactions().orEmpty().map {
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
}
return TimelineItemReactions(aggregatedReactions.toImmutableList())
}
private fun computeGroupPosition(
currentTimelineItem: MatrixTimelineItem.Event,
timelineItems: List<MatrixTimelineItem>,
index: Int
): MessagesItemGroupPosition {
val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem =
timelineItems.getOrNull(index + 1) as? MatrixTimelineItem.Event
val currentSender = currentTimelineItem.event.sender()
val previousSender = prevTimelineItem?.event?.sender()
val nextSender = nextTimelineItem?.event?.sender()
return when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
else -> MessagesItemGroupPosition.None
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import javax.inject.Inject
class TimelineItemVirtualFactory @Inject constructor() {
suspend fun create(
currentTimelineItem: MatrixTimelineItem.Virtual,
index: Int,
timelineItems: List<MatrixTimelineItem>,
): TimelineItem.Virtual {
return TimelineItem.Virtual(
id = "virtual_item_$index"
)
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.factories
import androidx.recyclerview.widget.DiffUtil
import io.element.android.features.messages.timeline.diff.CacheInvalidator
import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.util.invalidateLast
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis
class TimelineItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val eventItemFactory: TimelineItemEventFactory,
private val virtualItemFactory: TimelineItemVirtualFactory,
) {
private val timelineItems = MutableStateFlow<List<TimelineItem>>(emptyList())
private val timelineItemsCache = arrayListOf<TimelineItem?>()
// Items from rust sdk, used for diffing
private var matrixTimelineItems: List<MatrixTimelineItem> = emptyList()
private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
fun flow(): StateFlow<List<TimelineItem>> = timelineItems.asStateFlow()
suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>,
) = withContext(dispatchers.computation) {
lock.withLock {
calculateAndApplyDiff(timelineItems)
buildAndEmitTimelineItemStates(timelineItems)
}
}
suspend fun pushItem(
timelineItem: MatrixTimelineItem,
) = withContext(dispatchers.computation) {
lock.withLock {
// Makes sure to invalidate last as we need to recompute some data (like groupPosition)
timelineItemsCache.invalidateLast()
timelineItemsCache.add(null)
matrixTimelineItems = matrixTimelineItems + timelineItem
buildAndEmitTimelineItemStates(matrixTimelineItems)
}
}
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
val newTimelineItemStates = ArrayList<TimelineItem>()
for (index in timelineItemsCache.indices.reversed()) {
val cacheItem = timelineItemsCache[index]
if (cacheItem == null) {
buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
newTimelineItemStates.add(timelineItemState)
}
} else {
newTimelineItemStates.add(cacheItem)
}
}
this.timelineItems.emit(newTimelineItemStates)
}
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
val timeToDiff = measureTimeMillis {
val diffCallback =
MatrixTimelineItemsDiffCallback(
oldList = matrixTimelineItems,
newList = newTimelineItems
)
val diffResult = DiffUtil.calculateDiff(diffCallback, false)
matrixTimelineItems = newTimelineItems
diffResult.dispatchUpdatesTo(cacheInvalidator)
}
Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms")
}
private suspend fun buildAndCacheItem(
timelineItems: List<MatrixTimelineItem>,
index: Int
): TimelineItem? {
val timelineItemState =
when (val currentTimelineItem = timelineItems[index]) {
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems)
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem, index, timelineItems)
MatrixTimelineItem.Other -> null
}
timelineItemsCache[index] = timelineItemState
return timelineItemState
}
}

View file

@ -27,7 +27,7 @@ sealed interface TimelineItem {
val id: String val id: String
) : TimelineItem ) : TimelineItem
data class MessageEvent( data class Event(
val id: EventId, val id: EventId,
val senderId: String, val senderId: String,
val senderDisplayName: String?, val senderDisplayName: String?,

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.util
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
fun FormattedBody.toHtmlDocument(): Document? {
return takeIf { it.format == MessageFormat.HTML }?.body?.let { formattedBody ->
Jsoup.parse(formattedBody)
}
}

View file

@ -31,6 +31,6 @@ object MatrixModule {
@Provides @Provides
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
fun providesRustAuthenticationService(baseDirectory: File): AuthenticationService { fun providesRustAuthenticationService(baseDirectory: File): AuthenticationService {
return AuthenticationService(baseDirectory.absolutePath) return AuthenticationService(baseDirectory.absolutePath, null)
} }
} }

View file

@ -158,6 +158,12 @@ internal class RustRoomSummaryDataSource(
clear() clear()
addAll(diff.values.map { buildSummaryForRoomListEntry(it) }) addAll(diff.values.map { buildSummaryForRoomListEntry(it) })
} }
SlidingSyncViewRoomsListDiff.Pop -> {
removeLastOrNull()
}
SlidingSyncViewRoomsListDiff.Clear -> {
clear()
}
} }
} }

View file

@ -27,7 +27,7 @@ class RoomMessageFactory {
eventId = EventId(eventTimelineItem.eventId() ?: ""), eventId = EventId(eventTimelineItem.eventId() ?: ""),
body = eventTimelineItem.content().asMessage()?.body() ?: "", body = eventTimelineItem.content().asMessage()?.body() ?: "",
sender = UserId(eventTimelineItem.sender()), sender = UserId(eventTimelineItem.sender()),
originServerTs = eventTimelineItem.originServerTs()?.toLong() ?: 0L originServerTs = eventTimelineItem.timestamp().toLong()
) )
} }
} }

View file

@ -22,7 +22,6 @@ import org.matrix.rustcomponents.sdk.TimelineListener
interface MatrixTimeline { interface MatrixTimeline {
var callback: Callback? var callback: Callback?
val hasMoreToLoad: Boolean
interface Callback { interface Callback {
fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
@ -30,7 +29,7 @@ interface MatrixTimeline {
} }
fun timelineItems(): Flow<List<MatrixTimelineItem>> fun timelineItems(): Flow<List<MatrixTimelineItem>>
suspend fun paginateBackwards(count: Int): Result<Unit> suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
fun addListener(timelineListener: TimelineListener) fun addListener(timelineListener: TimelineListener)
fun initialize() fun initialize()
fun dispose() fun dispose()

View file

@ -18,15 +18,10 @@ package io.element.android.libraries.matrix.timeline
import org.matrix.rustcomponents.sdk.EventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineKey
sealed interface MatrixTimelineItem { sealed interface MatrixTimelineItem {
data class Event(val event: EventTimelineItem) : MatrixTimelineItem { data class Event(val event: EventTimelineItem) : MatrixTimelineItem {
val uniqueId: String val uniqueId: String = event.uniqueIdentifier()
get() = when (val eventKey = event.key()) {
is TimelineKey.TransactionId -> eventKey.txnId
is TimelineKey.EventId -> eventKey.eventId
}
} }
object Virtual : MatrixTimelineItem object Virtual : MatrixTimelineItem

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.timeline
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.RustMatrixRoom import io.element.android.libraries.matrix.room.RustMatrixRoom
import io.element.android.libraries.matrix.util.StoppableSpawnBag
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -26,14 +27,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOutcome import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.*
class RustMatrixTimeline( class RustMatrixTimeline(
private val matrixRoom: RustMatrixRoom, private val matrixRoom: RustMatrixRoom,
@ -45,20 +46,16 @@ class RustMatrixTimeline(
override var callback: MatrixTimeline.Callback? = null override var callback: MatrixTimeline.Callback? = null
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList()) MutableStateFlow(emptyList())
private val listenerTokens = StoppableSpawnBag()
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
override fun timelineItems(): Flow<List<MatrixTimelineItem>> { override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50) return timelineItems.sample(50)
} }
override val hasMoreToLoad: Boolean
get() {
return paginationOutcome.value.moreMessages
}
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) { private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
when (diff.change()) { when (diff.change()) {
TimelineChange.PUSH -> { TimelineChange.PUSH -> {
@ -107,12 +104,13 @@ class RustMatrixTimeline(
} }
} }
override suspend fun paginateBackwards(count: Int): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
if (!paginationOutcome.value.moreMessages) {
return@withContext Result.failure(IllegalStateException("no more message"))
}
runCatching { runCatching {
paginationOutcome.value = room.paginateBackwards(count.toUShort()) val paginationOptions = PaginationOptions.UntilNumItems(
eventLimit = requestSize.toUShort(),
items = untilNumberOfItems.toUShort()
)
room.paginateBackwards(paginationOptions)
} }
} }
@ -124,7 +122,7 @@ class RustMatrixTimeline(
} }
override fun addListener(timelineListener: TimelineListener) { override fun addListener(timelineListener: TimelineListener) {
slidingSyncRoom.addTimelineListener(timelineListener) listenerTokens += slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings = null)
} }
override fun initialize() { override fun initialize() {
@ -132,7 +130,7 @@ class RustMatrixTimeline(
} }
override fun dispose() { override fun dispose() {
slidingSyncRoom.removeTimeline() listenerTokens.dispose()
} }
/** /**

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.util
import org.matrix.rustcomponents.sdk.StoppableSpawn
import java.util.concurrent.CopyOnWriteArraySet
class StoppableSpawnBag(private val tokens: MutableSet<StoppableSpawn> = CopyOnWriteArraySet()) : Set<StoppableSpawn> by tokens {
operator fun plusAssign(stoppableSpawn: StoppableSpawn?) {
if (stoppableSpawn == null) return
tokens += stoppableSpawn
}
fun dispose() {
tokens.forEach { it.cancel() }
tokens.clear()
}
}

View file

@ -29,14 +29,11 @@ class FakeMatrixTimeline : MatrixTimeline {
get() = null get() = null
set(value) {} set(value) {}
override val hasMoreToLoad: Boolean
get() = true
override fun timelineItems(): Flow<List<MatrixTimelineItem>> { override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return emptyFlow() return emptyFlow()
} }
override suspend fun paginateBackwards(count: Int): Result<Unit> { override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
return Result.success(Unit) return Result.success(Unit)
} }