Start integrating uptodate rust sdk and make it compile (WIP)
This commit is contained in:
parent
95b8cb6c03
commit
f0db8b00ea
34 changed files with 739 additions and 352 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue