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 a4f9354e8a
commit 0f498a0290
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
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) {
when (event) {
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState)
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
}
}
return MessagesState(
@ -94,7 +94,7 @@ class MessagesPresenter @Inject constructor(
fun CoroutineScope.handleTimelineAction(
action: TimelineItemAction,
targetEvent: TimelineItem.MessageEvent,
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
) = launch {
when (action) {
@ -110,11 +110,11 @@ class MessagesPresenter @Inject constructor(
Timber.v("NotImplementedYet")
}
private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) {
private suspend fun handleActionRedact(event: TimelineItem.Event) {
room.redactEvent(event.id)
}
private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Edit(
targetEvent.id,
(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, "")
composerState.eventSink(
MessageComposerEvents.SetMode(composerMode)

View file

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

View file

@ -20,5 +20,5 @@ import io.element.android.features.messages.timeline.model.TimelineItem
sealed interface 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) {
when (event) {
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)
val actions =
if (timelineItem.content is TimelineItemRedactedContent) {

View file

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

View file

@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
fun ActionListView(
state: ActionListState,
modalBottomSheetState: ModalBottomSheetState,
onActionSelected: (action: TimelineItemAction, TimelineItem.MessageEvent) -> Unit,
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
@ -65,7 +65,7 @@ fun ActionListView(
fun onItemActionClicked(
itemAction: TimelineItemAction,
targetItem: TimelineItem.MessageEvent
targetItem: TimelineItem.Event
) {
onActionSelected(itemAction, targetItem)
coroutineScope.launch {
@ -92,7 +92,7 @@ fun ActionListView(
private fun SheetContent(
state: ActionListState,
modifier: Modifier = Modifier,
onActionClicked: (TimelineItemAction, TimelineItem.MessageEvent) -> Unit = { _, _ -> },
onActionClicked: (TimelineItemAction, TimelineItem.Event) -> Unit = { _, _ -> },
) {
when (val target = state.target) {
is ActionListState.Target.Loading,
@ -110,7 +110,7 @@ private fun SheetContent(
) { action ->
ListItem(
modifier = Modifier.clickable {
onActionClicked(action, target.messageEvent)
onActionClicked(action, target.event)
},
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.rememberCoroutineScope
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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@ -39,18 +37,15 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val PAGINATION_COUNT = 50
private const val backPaginationEventLimit = 20
private const val backPaginationPageSize = 50
class TimelinePresenter @Inject constructor(
coroutineDispatchers: CoroutineDispatchers,
client: MatrixClient,
private val timelineItemsFactory: TimelineItemsFactory,
room: MatrixRoom,
) : Presenter<TimelineState> {
private val timeline = room.timeline()
private val matrixItemHelper = MatrixItemHelper(client)
private val timelineItemsFactory =
TimelineItemsFactory(matrixItemHelper, room, coroutineDispatchers.computation)
private class TimelineCallback(
private val coroutineScope: CoroutineScope,
@ -66,9 +61,6 @@ class TimelinePresenter @Inject constructor(
@Composable
override fun present(): TimelineState {
val localCoroutineScope = rememberCoroutineScope()
val hasMoreToLoad = rememberSaveable {
mutableStateOf(timeline.hasMoreToLoad)
}
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
mutableStateOf(null)
}
@ -78,7 +70,7 @@ class TimelinePresenter @Inject constructor(
fun handleEvents(event: TimelineEvents) {
when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(hasMoreToLoad)
TimelineEvents.LoadMore -> localCoroutineScope.loadMore()
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
}
}
@ -102,13 +94,11 @@ class TimelinePresenter @Inject constructor(
return TimelineState(
highlightedEventId = highlightedEventId.value,
timelineItems = timelineItems.value.toImmutableList(),
hasMoreToLoad = hasMoreToLoad.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch {
timeline.paginateBackwards(PAGINATION_COUNT)
hasMoreToLoad.value = timeline.hasMoreToLoad
private fun CoroutineScope.loadMore() = launch {
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
}
}

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ class RoomMessageFactory {
eventId = EventId(eventTimelineItem.eventId() ?: ""),
body = eventTimelineItem.content().asMessage()?.body() ?: "",
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 {
var callback: Callback?
val hasMoreToLoad: Boolean
interface Callback {
fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
@ -30,7 +29,7 @@ interface MatrixTimeline {
}
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 initialize()
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.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineKey
sealed interface MatrixTimelineItem {
data class Event(val event: EventTimelineItem) : MatrixTimelineItem {
val uniqueId: String
get() = when (val eventKey = event.key()) {
is TimelineKey.TransactionId -> eventKey.txnId
is TimelineKey.EventId -> eventKey.eventId
}
val uniqueId: String = event.uniqueIdentifier()
}
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.matrix.core.EventId
import io.element.android.libraries.matrix.room.RustMatrixRoom
import io.element.android.libraries.matrix.util.StoppableSpawnBag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
@ -26,14 +27,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
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.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber
import java.util.Collections
import java.util.*
class RustMatrixTimeline(
private val matrixRoom: RustMatrixRoom,
@ -45,20 +46,16 @@ class RustMatrixTimeline(
override var callback: MatrixTimeline.Callback? = null
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
private val listenerTokens = StoppableSpawnBag()
@OptIn(FlowPreview::class)
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50)
}
override val hasMoreToLoad: Boolean
get() {
return paginationOutcome.value.moreMessages
}
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
when (diff.change()) {
TimelineChange.PUSH -> {
@ -107,12 +104,13 @@ class RustMatrixTimeline(
}
}
override suspend fun paginateBackwards(count: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
if (!paginationOutcome.value.moreMessages) {
return@withContext Result.failure(IllegalStateException("no more message"))
}
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
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) {
slidingSyncRoom.addTimelineListener(timelineListener)
listenerTokens += slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings = null)
}
override fun initialize() {
@ -132,7 +130,7 @@ class RustMatrixTimeline(
}
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
set(value) {}
override val hasMoreToLoad: Boolean
get() = true
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return emptyFlow()
}
override suspend fun paginateBackwards(count: Int): Result<Unit> {
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
return Result.success(Unit)
}