Merge pull request #112 from vector-im/feature/fga/update_rust_sdk
Feature/fga/update rust sdk
This commit is contained in:
@@ -26,7 +26,7 @@ maestro test \
|
||||
-e USERNAME=user \
|
||||
-e PASSWORD=123 \
|
||||
-e ROOM_NAME="my room" \
|
||||
.maestro/allTest.yaml
|
||||
.maestro/allTests.yaml
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
@@ -45,6 +45,7 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/
|
||||
## Build instructions
|
||||
|
||||
Just clone the project and open it in Android Studio.
|
||||
Makes sure to select the `app` configuration when building (as we also have sample apps in the project).
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.element.android.x"
|
||||
targetSdk = 33 // TODO Use Versions.targetSdk
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
targetSdk = Versions.targetSdk
|
||||
versionCode = Versions.versionCode
|
||||
versionName = Versions.versionName
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@@ -109,24 +109,9 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composecompiler.get()
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting for https://github.com/google/ksp/issues/37
|
||||
applicationVariants.all {
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,8 +67,11 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
|
||||
|
||||
private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState<LoggedInState>) = launch {
|
||||
loggedInState.value = LoggedInState.LoggingIn
|
||||
try {
|
||||
//TODO rework the setHomeserver flow
|
||||
tryOrNull {
|
||||
authenticationService.setHomeserver(homeserver)
|
||||
}
|
||||
try {
|
||||
val sessionId = authenticationService.login(formState.login.trim(), formState.password.trim())
|
||||
loggedInState.value = LoggedInState.LoggedIn(sessionId)
|
||||
} catch (failure: Throwable) {
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies {
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.textcomposer)
|
||||
implementation(projects.libraries.dateformatter)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import io.element.android.features.messages.textcomposer.MessageComposerState
|
||||
import io.element.android.features.messages.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -79,7 +79,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(
|
||||
@@ -95,7 +95,7 @@ class MessagesPresenter @Inject constructor(
|
||||
|
||||
fun CoroutineScope.handleTimelineAction(
|
||||
action: TimelineItemAction,
|
||||
targetEvent: TimelineItem.MessageEvent,
|
||||
targetEvent: TimelineItem.Event,
|
||||
composerState: MessageComposerState,
|
||||
) = launch {
|
||||
when (action) {
|
||||
@@ -111,13 +111,15 @@ class MessagesPresenter @Inject constructor(
|
||||
Timber.v("NotImplementedYet")
|
||||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) {
|
||||
room.redactEvent(event.id)
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
room.redactEvent(event.eventId)
|
||||
}
|
||||
|
||||
private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
|
||||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
if (targetEvent.eventId == null) return
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.id,
|
||||
targetEvent.eventId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty()
|
||||
)
|
||||
composerState.eventSink(
|
||||
@@ -125,8 +127,9 @@ class MessagesPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleActionReply(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
|
||||
val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "")
|
||||
private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
if (targetEvent.eventId == null) return
|
||||
val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.eventId, "")
|
||||
composerState.eventSink(
|
||||
MessageComposerEvents.SetMode(composerMode)
|
||||
)
|
||||
|
||||
@@ -45,7 +45,6 @@ fun aMessagesState() = MessagesState(
|
||||
),
|
||||
timelineState = aTimelineState().copy(
|
||||
timelineItems = aTimelineItemList(aTimelineItemContent()),
|
||||
hasMoreToLoad = false,
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
eventSink = {}
|
||||
|
||||
@@ -88,21 +88,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(
|
||||
@@ -142,8 +142,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -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) {
|
||||
|
||||
@@ -28,9 +28,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
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ package io.element.android.features.messages.actionlist
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.timeline.aMessageEvent
|
||||
import io.element.android.features.messages.timeline.aTimelineItemEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
override val values: Sequence<ActionListState>
|
||||
get() = sequenceOf(
|
||||
anActionListState(),
|
||||
anActionListState().copy(target = ActionListState.Target.Loading(aMessageEvent())),
|
||||
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
messageEvent = aMessageEvent(),
|
||||
event = aTimelineItemEvent(),
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
|
||||
@@ -53,7 +53,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()
|
||||
@@ -67,7 +67,7 @@ fun ActionListView(
|
||||
|
||||
fun onItemActionClicked(
|
||||
itemAction: TimelineItemAction,
|
||||
targetItem: TimelineItem.MessageEvent
|
||||
targetItem: TimelineItem.Event
|
||||
) {
|
||||
onActionSelected(itemAction, targetItem)
|
||||
coroutineScope.launch {
|
||||
@@ -94,7 +94,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,
|
||||
@@ -112,7 +112,7 @@ private fun SheetContent(
|
||||
) { action ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
onActionClicked(action, target.messageEvent)
|
||||
onActionClicked(action, target.event)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
|
||||
@@ -1,253 +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.TimelineItemGroupPosition
|
||||
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(
|
||||
id = currentSender,
|
||||
name = senderDisplayName,
|
||||
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
|
||||
): TimelineItemGroupPosition {
|
||||
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 -> TimelineItemGroupPosition.First
|
||||
previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
|
||||
previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
|
||||
else -> TimelineItemGroupPosition.None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,61 +24,46 @@ 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
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
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,
|
||||
private val timelineItemsFactory: TimelineItemsFactory,
|
||||
) : MatrixTimeline.Callback {
|
||||
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
|
||||
coroutineScope.launch {
|
||||
timelineItemsFactory.pushItem(timelineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): TimelineState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val hasMoreToLoad = rememberSaveable {
|
||||
mutableStateOf(timeline.hasMoreToLoad)
|
||||
}
|
||||
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val timelineItems = timelineItemsFactory
|
||||
.flow()
|
||||
.collectAsState(emptyList())
|
||||
.collectAsState()
|
||||
|
||||
val paginationState = timeline
|
||||
.paginationState()
|
||||
.collectAsState()
|
||||
|
||||
fun handleEvents(event: TimelineEvents) {
|
||||
when (event) {
|
||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(hasMoreToLoad)
|
||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
|
||||
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
||||
}
|
||||
}
|
||||
@@ -87,28 +72,34 @@ class TimelinePresenter @Inject constructor(
|
||||
timeline
|
||||
.timelineItems()
|
||||
.onEach(timelineItemsFactory::replaceWith)
|
||||
.onEach { timelineItems ->
|
||||
if (timelineItems.isEmpty()) {
|
||||
loadMore(paginationState.value)
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.callback = TimelineCallback(localCoroutineScope, timelineItemsFactory)
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.callback = null
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
paginationState = paginationState.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(paginationState: MatrixTimeline.PaginationState) = launch {
|
||||
if (paginationState.canBackPaginate && !paginationState.isBackPaginating) {
|
||||
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
|
||||
} else {
|
||||
Timber.v("Can't back paginate as paginationState = $paginationState")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ package io.element.android.features.messages.timeline
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimeline
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class TimelineState(
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val hasMoreToLoad: Boolean,
|
||||
val highlightedEventId: EventId?,
|
||||
val paginationState: MatrixTimeline.PaginationState,
|
||||
val eventSink: (TimelineEvents) -> Unit
|
||||
)
|
||||
|
||||
@@ -17,53 +17,54 @@
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
import io.element.android.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
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.TimelineItemTextContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimeline
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun aTimelineState() = TimelineState(
|
||||
timelineItems = persistentListOf(),
|
||||
hasMoreToLoad = false,
|
||||
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, canBackPaginate = true),
|
||||
highlightedEventId = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
internal fun aTimelineItemList(content: TimelineItemContent): ImmutableList<TimelineItem> {
|
||||
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
|
||||
return persistentListOf(
|
||||
// 3 items (First Middle Last) with isMine = false
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Last
|
||||
),
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle
|
||||
),
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// 3 items (First Middle Last) with isMine = true
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Last
|
||||
),
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle
|
||||
),
|
||||
aMessageEvent(
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
@@ -71,13 +72,15 @@ internal fun aTimelineItemList(content: TimelineItemContent): ImmutableList<Time
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aMessageEvent(
|
||||
internal fun aTimelineItemEvent(
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemContent = aTimelineItemContent(),
|
||||
content: TimelineItemEventContent = aTimelineItemContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
|
||||
): TimelineItem.MessageEvent {
|
||||
return TimelineItem.MessageEvent(
|
||||
id = EventId(Math.random().toString()),
|
||||
): TimelineItem.Event {
|
||||
val randomId = Math.random().toString()
|
||||
return TimelineItem.Event(
|
||||
id = randomId,
|
||||
eventId = EventId(randomId),
|
||||
senderId = "@senderId",
|
||||
senderAvatar = AvatarData("@senderId", "sender"),
|
||||
content = content,
|
||||
@@ -92,7 +95,7 @@ internal fun aMessageEvent(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemContent(): TimelineItemContent {
|
||||
internal fun aTimelineItemContent(): TimelineItemEventContent {
|
||||
return TimelineItemTextContent(
|
||||
body = "Text",
|
||||
htmlDocument = null
|
||||
|
||||
@@ -34,7 +34,7 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -54,27 +54,21 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.features.messages.timeline.components.BubbleState
|
||||
import io.element.android.features.messages.timeline.components.MessageEventBubble
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemImageView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemTextView
|
||||
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
|
||||
import io.element.android.features.messages.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||
import io.element.android.features.messages.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContentProvider
|
||||
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.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
@@ -86,9 +80,14 @@ 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 = {},
|
||||
) {
|
||||
|
||||
fun onReachedLoadMore() {
|
||||
state.eventSink(TimelineEvents.LoadMore)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
@@ -98,29 +97,23 @@ fun TimelineView(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
reverseLayout = true
|
||||
) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.timelineItems,
|
||||
contentType = { timelineItem -> timelineItem.contentType() },
|
||||
key = { timelineItem -> timelineItem.key() },
|
||||
) { timelineItem ->
|
||||
contentType = { _, timelineItem -> timelineItem.contentType() },
|
||||
key = { _, timelineItem -> timelineItem.key() },
|
||||
) { index, timelineItem ->
|
||||
TimelineItemRow(
|
||||
timelineItem = timelineItem,
|
||||
isHighlighted = timelineItem.key() == state.highlightedEventId?.value,
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked
|
||||
)
|
||||
}
|
||||
if (state.hasMoreToLoad) {
|
||||
item {
|
||||
TimelineLoadingMoreIndicator()
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
onReachedLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onReachedLoadMore() {
|
||||
state.eventSink(TimelineEvents.LoadMore)
|
||||
}
|
||||
|
||||
TimelineScrollHelper(
|
||||
lazyListState = lazyListState,
|
||||
timelineItems = state.timelineItems,
|
||||
@@ -131,14 +124,15 @@ fun TimelineView(
|
||||
|
||||
private fun TimelineItem.key(): String {
|
||||
return when (this) {
|
||||
is TimelineItem.MessageEvent -> id.value
|
||||
is TimelineItem.Event -> id
|
||||
is TimelineItem.Virtual -> id
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimelineItem.contentType(): Int {
|
||||
// Todo optimize for each subtype
|
||||
return when (this) {
|
||||
is TimelineItem.MessageEvent -> 0
|
||||
is TimelineItem.Event -> 0
|
||||
is TimelineItem.Virtual -> 1
|
||||
}
|
||||
}
|
||||
@@ -147,30 +141,55 @@ 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,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) }
|
||||
is TimelineItem.Virtual -> TimelineItemVirtualRow(
|
||||
virtual = timelineItem
|
||||
)
|
||||
is TimelineItem.Event -> {
|
||||
|
||||
fun onClick() {
|
||||
onClick(timelineItem)
|
||||
}
|
||||
|
||||
fun onLongClick() {
|
||||
onLongClick(timelineItem)
|
||||
}
|
||||
|
||||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageEventRow(
|
||||
messageEvent: TimelineItem.MessageEvent,
|
||||
fun TimelineItemVirtualRow(
|
||||
virtual: TimelineItem.Virtual,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (virtual.model) {
|
||||
is TimelineItemLoadingModel -> TimelineLoadingMoreIndicator(modifier)
|
||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
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)
|
||||
@@ -182,21 +201,21 @@ 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(
|
||||
state = BubbleState(
|
||||
groupPosition = messageEvent.groupPosition,
|
||||
isMine = messageEvent.isMine,
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
@@ -207,45 +226,21 @@ fun MessageEventRow(
|
||||
.widthIn(max = 320.dp)
|
||||
) {
|
||||
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
when (messageEvent.content) {
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = messageEvent.content,
|
||||
interactionSource = interactionSource,
|
||||
modifier = contentModifier,
|
||||
onTextClicked = onClick,
|
||||
onTextLongClicked = onLongClick
|
||||
)
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
TimelineItemEventContentView(event.content, interactionSource, onClick, onLongClick, 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))
|
||||
@@ -332,41 +327,24 @@ internal fun BoxScope.TimelineScrollHelper(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TimelineLoadingMoreIndicator() {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenLightPreview(
|
||||
@PreviewParameter(TimelineItemContentProvider::class) content: TimelineItemContent
|
||||
fun TimelineViewLightPreview(
|
||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreviewLight { ContentToPreview(content) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenDarkPreview(
|
||||
@PreviewParameter(TimelineItemContentProvider::class) content: TimelineItemContent
|
||||
fun TimelineViewDarkPreview(
|
||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreviewDark { ContentToPreview(content) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(content: TimelineItemContent) {
|
||||
private fun ContentToPreview(content: TimelineItemEventContent) {
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
TimelineView(
|
||||
state = aTimelineState().copy(
|
||||
timelineItems = timelineItems,
|
||||
hasMoreToLoad = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.timeline.model.bubble.BubbleStateProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.components.event
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventContentView(
|
||||
content: TimelineItemEventContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (content) {
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = content,
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
onTextClicked = onClick,
|
||||
onTextLongClicked = onLongClick
|
||||
)
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
@@ -32,8 +32,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContentProvider
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContentProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.style.URLSpan
|
||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.features.messages.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.components.event
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.components.virtual
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModelProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorView(
|
||||
model: TimelineItemDaySeparatorModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = model.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewLightPreview(@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel) =
|
||||
ElementPreviewLight { ContentToPreview(model) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewDarkPreview(@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel) =
|
||||
ElementPreviewDark { ContentToPreview(model) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(model: TimelineItemDaySeparatorModel) {
|
||||
TimelineItemDaySeparatorView(
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.components.virtual
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
|
||||
@Composable
|
||||
internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineLoadingMoreIndicatorLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineLoadingMoreIndicatorDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineLoadingMoreIndicator()
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.factories.event.TimelineItemEventFactory
|
||||
import io.element.android.features.messages.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
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): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentFailedToParseMessageFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.FailedToParseMessageLike): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentFailedToParseStateFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.FailedToParseState): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEmoteContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemNoticeContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.timeline.model.event.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?): TimelineItemEventContent {
|
||||
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,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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentProfileChangeFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.ProfileChange): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentRedactedFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.RedactedMessage): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentRoomMembershipFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.RoomMembership): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentStateFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.State): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentStickerFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.Sticker): TimelineItemEventContent {
|
||||
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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentUTDFactory @Inject constructor() {
|
||||
|
||||
fun create(kind: TimelineItemContentKind.UnableToDecrypt): TimelineItemEventContent {
|
||||
return TimelineItemEncryptedContent(kind.msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.event
|
||||
|
||||
import io.element.android.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
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.timeline.MatrixTimelineItem
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.ProfileTimelineDetails
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemEventFactory @Inject constructor(
|
||||
private val contentFactory: TimelineItemContentFactory,
|
||||
) {
|
||||
|
||||
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: String?
|
||||
val senderAvatarUrl: String?
|
||||
|
||||
when (val senderProfile = currentTimelineItem.event.senderProfile()) {
|
||||
ProfileTimelineDetails.Unavailable,
|
||||
ProfileTimelineDetails.Pending,
|
||||
is ProfileTimelineDetails.Error -> {
|
||||
senderDisplayName = null
|
||||
senderAvatarUrl = null
|
||||
}
|
||||
is ProfileTimelineDetails.Ready -> {
|
||||
senderDisplayName = senderProfile.displayName
|
||||
senderAvatarUrl = senderProfile.avatarUrl
|
||||
}
|
||||
}
|
||||
|
||||
val senderAvatarData = AvatarData(
|
||||
id = currentSender,
|
||||
name = senderDisplayName ?: currentSender,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
return TimelineItem.Event(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
eventId = currentTimelineItem.eventId,
|
||||
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
|
||||
): TimelineItemGroupPosition {
|
||||
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 -> TimelineItemGroupPosition.First
|
||||
previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
|
||||
previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
|
||||
else -> TimelineItemGroupPosition.None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.features.messages.timeline.factories.virtual
|
||||
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.dateformatter.DaySeparatorFormatter
|
||||
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
|
||||
|
||||
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
|
||||
val formattedDate = daySeparatorFormatter.format(virtualItem.ts.toLong())
|
||||
return TimelineItemDaySeparatorModel(
|
||||
formattedDate = formattedDate
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.virtual
|
||||
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemUnknownVirtualModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemVirtualFactory @Inject constructor(
|
||||
private val daySeparatorFactory: TimelineItemDaySeparatorFactory,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
currentTimelineItem: MatrixTimelineItem.Virtual,
|
||||
index: Int,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
): TimelineItem.Virtual {
|
||||
return TimelineItem.Virtual(
|
||||
id = "virtual_item_$index",
|
||||
model = currentTimelineItem.computeModel(index)
|
||||
)
|
||||
}
|
||||
|
||||
private fun MatrixTimelineItem.Virtual.computeModel(index: Int): TimelineItemVirtualModel {
|
||||
return when (val inner = virtual) {
|
||||
is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner)
|
||||
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
|
||||
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingModel
|
||||
is VirtualTimelineItem.TimelineStart -> TimelineItemReadMarkerModel
|
||||
else -> TimelineItemUnknownVirtualModel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,33 @@
|
||||
package io.element.android.features.messages.timeline.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
|
||||
fun identifier(): String = when(this){
|
||||
is Event -> id
|
||||
is Virtual -> id
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Virtual(
|
||||
val id: String
|
||||
val id: String,
|
||||
val model: TimelineItemVirtualModel
|
||||
) : TimelineItem
|
||||
|
||||
data class MessageEvent(
|
||||
val id: EventId,
|
||||
@Immutable
|
||||
data class Event(
|
||||
val id: String,
|
||||
val eventId: EventId? = null,
|
||||
val senderId: String,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatar: AvatarData,
|
||||
val content: TimelineItemContent,
|
||||
val content: TimelineItemEventContent,
|
||||
val sentTime: String = "",
|
||||
val isMine: Boolean = false,
|
||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.model.bubble
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
|
||||
@Stable
|
||||
data class BubbleState(
|
||||
val groupPosition: TimelineItemGroupPosition,
|
||||
val isMine: Boolean,
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components
|
||||
package io.element.android.features.messages.timeline.model.bubble
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
data class TimelineItemEncryptedContent(
|
||||
val encryptedMessage: EncryptedMessage
|
||||
) : TimelineItemContent
|
||||
) : TimelineItemEventContent
|
||||
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
sealed interface TimelineItemContent
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItemEventContent
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import org.jsoup.Jsoup
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
class TimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
|
||||
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
|
||||
override val values = sequenceOf(
|
||||
aTimelineItemEmoteContent(),
|
||||
aTimelineItemEncryptedContent(),
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.media.MediaResolver
|
||||
|
||||
@@ -23,4 +23,4 @@ data class TimelineItemImageContent(
|
||||
val imageMeta: MediaResolver.Meta,
|
||||
val blurhash: String?,
|
||||
val aspectRatio: Float
|
||||
) : TimelineItemContent
|
||||
) : TimelineItemEventContent
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.media.MediaResolver
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
object TimelineItemRedactedContent : TimelineItemContent
|
||||
object TimelineItemRedactedContent : TimelineItemEventContent
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
sealed interface TimelineItemTextBasedContent : TimelineItemContent {
|
||||
sealed interface TimelineItemTextBasedContent : TimelineItemEventContent {
|
||||
val body: String
|
||||
val htmlDocument: Document?
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.model.content
|
||||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
object TimelineItemUnknownContent : TimelineItemContent
|
||||
object TimelineItemUnknownContent : TimelineItemEventContent
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
data class TimelineItemDaySeparatorModel(
|
||||
val formattedDate: String
|
||||
) : TimelineItemVirtualModel
|
||||
@@ -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.model.virtual
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class TimelineItemDaySeparatorModelProvider : PreviewParameterProvider<TimelineItemDaySeparatorModel> {
|
||||
override val values = sequenceOf(
|
||||
aTimelineItemDaySeparatorModel("Today"),
|
||||
aTimelineItemDaySeparatorModel("March 6, 2023")
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemDaySeparatorModel(formattedDate: String) = TimelineItemDaySeparatorModel(
|
||||
formattedDate = formattedDate
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
object TimelineItemLoadingModel : TimelineItemVirtualModel
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
object TimelineItemReadMarkerModel : TimelineItemVirtualModel
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
object TimelineItemTimelineStartModel : TimelineItemVirtualModel
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
object TimelineItemUnknownVirtualModel : TimelineItemVirtualModel
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItemVirtualModel
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,14 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.textcomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.timeline.TimelinePresenter
|
||||
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.TimelineItemTextContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.room.MatrixRoom
|
||||
@@ -38,7 +40,6 @@ import io.element.android.libraries.matrixtest.A_MESSAGE
|
||||
import io.element.android.libraries.matrixtest.A_ROOM_ID
|
||||
import io.element.android.libraries.matrixtest.A_USER_ID
|
||||
import io.element.android.libraries.matrixtest.A_USER_NAME
|
||||
import io.element.android.libraries.matrixtest.FakeMatrixClient
|
||||
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -134,14 +135,13 @@ class MessagesPresenterTest {
|
||||
private fun TestScope.createMessagePresenter(
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom()
|
||||
): MessagesPresenter {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val messageComposerPresenter = MessageComposerPresenter(
|
||||
appCoroutineScope = this,
|
||||
room = matrixRoom
|
||||
)
|
||||
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
client = matrixClient,
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = matrixRoom,
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter()
|
||||
@@ -154,25 +154,3 @@ class MessagesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Move to common module to reuse
|
||||
fun testCoroutineDispatchers() = CoroutineDispatchers(
|
||||
io = UnconfinedTestDispatcher(),
|
||||
computation = UnconfinedTestDispatcher(),
|
||||
main = UnconfinedTestDispatcher(),
|
||||
diffUpdateDispatcher = UnconfinedTestDispatcher(),
|
||||
)
|
||||
|
||||
// TODO Move to common module to reuse and remove this duplication
|
||||
private fun aMessageEvent(
|
||||
isMine: Boolean = true,
|
||||
content: TimelineItemContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null),
|
||||
) = TimelineItem.MessageEvent(
|
||||
id = AN_EVENT_ID,
|
||||
senderId = A_USER_ID.value,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
reactionsState = TimelineItemReactions(persistentListOf())
|
||||
)
|
||||
|
||||
@@ -25,9 +25,9 @@ import com.google.common.truth.Truth.assertThat
|
||||
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.TimelineItemReactions
|
||||
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.TimelineItemTextContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrixtest.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrixtest.A_MESSAGE
|
||||
@@ -37,6 +37,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
|
||||
class ActionListPresenterTest {
|
||||
@Test
|
||||
@@ -163,9 +164,10 @@ class ActionListPresenterTest {
|
||||
|
||||
private fun aMessageEvent(
|
||||
isMine: Boolean,
|
||||
content: TimelineItemContent,
|
||||
) = TimelineItem.MessageEvent(
|
||||
id = AN_EVENT_ID,
|
||||
content: TimelineItemEventContent,
|
||||
) = TimelineItem.Event(
|
||||
id = AN_EVENT_ID.value,
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID.value,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.fakes
|
||||
|
||||
import io.element.android.libraries.dateformatter.DaySeparatorFormatter
|
||||
|
||||
class FakeDaySeparatorFormatter : DaySeparatorFormatter {
|
||||
|
||||
private var format = ""
|
||||
|
||||
fun givenFormat(format: String) {
|
||||
this.format = format
|
||||
}
|
||||
|
||||
override fun format(timestamp: Long): String {
|
||||
return format
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.fixtures
|
||||
|
||||
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.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrixtest.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrixtest.A_MESSAGE
|
||||
import io.element.android.libraries.matrixtest.A_USER_ID
|
||||
import io.element.android.libraries.matrixtest.A_USER_NAME
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal fun aMessageEvent(
|
||||
isMine: Boolean = true,
|
||||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null),
|
||||
) = TimelineItem.Event(
|
||||
id = AN_EVENT_ID.value,
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID.value,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
reactionsState = TimelineItemReactions(persistentListOf())
|
||||
)
|
||||
@@ -14,16 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.timeline
|
||||
package io.element.android.features.messages.fixtures
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
|
||||
// TODO Move to common module to reuse
|
||||
fun testCoroutineDispatchers() = CoroutineDispatchers(
|
||||
internal fun testCoroutineDispatchers() = CoroutineDispatchers(
|
||||
io = UnconfinedTestDispatcher(),
|
||||
computation = UnconfinedTestDispatcher(),
|
||||
main = UnconfinedTestDispatcher(),
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.fixtures
|
||||
|
||||
import io.element.android.features.messages.fakes.FakeDaySeparatorFormatter
|
||||
import io.element.android.features.messages.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFailedToParseStateFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentMessageFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentProfileChangeFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentRedactedFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentRoomMembershipFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentStateFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentStickerFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemContentUTDFactory
|
||||
import io.element.android.features.messages.timeline.factories.event.TimelineItemEventFactory
|
||||
import io.element.android.features.messages.timeline.factories.virtual.TimelineItemDaySeparatorFactory
|
||||
import io.element.android.features.messages.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||
|
||||
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(),
|
||||
stateFactory = TimelineItemContentStateFactory(),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -22,10 +22,8 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.libraries.matrixtest.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrixtest.FakeMatrixClient
|
||||
import io.element.android.libraries.matrixtest.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -36,52 +34,69 @@ class TimelinePresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
testCoroutineDispatchers(),
|
||||
FakeMatrixClient(),
|
||||
FakeMatrixRoom()
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.timelineItems).isEmpty()
|
||||
val loadedNoTimelineState = awaitItem()
|
||||
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val matrixTimeline = FakeMatrixTimeline()
|
||||
val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline)
|
||||
fun `present - makes sure timeline is initialized and disposed`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline()
|
||||
val presenter = TimelinePresenter(
|
||||
testCoroutineDispatchers(),
|
||||
FakeMatrixClient(),
|
||||
matrixRoom
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(matrixTimeline = fakeTimeline),
|
||||
)
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
assertThat(fakeTimeline.isInitialized).isTrue()
|
||||
}
|
||||
assertThat(fakeTimeline.isInitialized).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasMoreToLoad).isTrue()
|
||||
matrixTimeline.givenHasMoreToLoad(false)
|
||||
assertThat(initialState.paginationState.canBackPaginate).isTrue()
|
||||
assertThat(initialState.paginationState.isBackPaginating).isFalse()
|
||||
initialState.eventSink.invoke(TimelineEvents.LoadMore)
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.hasMoreToLoad).isFalse()
|
||||
val inPaginationState = awaitItem()
|
||||
assertThat(inPaginationState.paginationState.isBackPaginating).isTrue()
|
||||
assertThat(inPaginationState.paginationState.canBackPaginate).isTrue()
|
||||
val postPaginationState = awaitItem()
|
||||
assertThat(postPaginationState.paginationState.canBackPaginate).isTrue()
|
||||
assertThat(postPaginationState.paginationState.isBackPaginating).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - set highlighted event`() = runTest {
|
||||
val matrixTimeline = FakeMatrixTimeline()
|
||||
val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline)
|
||||
val presenter = TimelinePresenter(
|
||||
testCoroutineDispatchers(),
|
||||
FakeMatrixClient(),
|
||||
matrixRoom
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
assertThat(initialState.highlightedEventId).isNull()
|
||||
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID))
|
||||
val withHighlightedState = awaitItem()
|
||||
@@ -91,26 +106,4 @@ class TimelinePresenterTest {
|
||||
assertThat(withoutHighlightedState.highlightedEventId).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - test callback`() = runTest {
|
||||
val matrixTimeline = FakeMatrixTimeline()
|
||||
val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline)
|
||||
val presenter = TimelinePresenter(
|
||||
testCoroutineDispatchers(),
|
||||
FakeMatrixClient(),
|
||||
matrixRoom
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.timelineItems).isEmpty()
|
||||
// Simulate callback from the SDK
|
||||
matrixTimeline.callback?.onPushedTimelineItem(MatrixTimelineItem.Virtual)
|
||||
val nonEmptyState = awaitItem()
|
||||
assertThat(nonEmptyState.timelineItems).isNotEmpty()
|
||||
assertThat(nonEmptyState.timelineItems[0]).isEqualTo(TimelineItem.Virtual("virtual_item_0"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.2"
|
||||
|
||||
# Di
|
||||
inject = "javax.inject:javax.inject:1"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.dateformatter
|
||||
|
||||
interface DaySeparatorFormatter {
|
||||
fun format(timestamp: Long): String
|
||||
}
|
||||
@@ -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.libraries.dateformatter.impl
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import java.time.Period
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
// TODO rework this date formatting
|
||||
class DateFormatters @Inject constructor(
|
||||
private val locale: Locale,
|
||||
private val clock: Clock,
|
||||
private val timeZone: TimeZone,
|
||||
) {
|
||||
|
||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
internal fun formatTime(localDateTime: LocalDateTime): String {
|
||||
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
internal fun formatDate(
|
||||
dateToFormat: LocalDateTime,
|
||||
currentDate: LocalDateTime,
|
||||
useRelative: Boolean
|
||||
): String {
|
||||
val period = Period.between(dateToFormat.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate())
|
||||
return if (period.years.absoluteValue >= 1) {
|
||||
formatDateWithYear(dateToFormat)
|
||||
} else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
|
||||
getRelativeDay(dateToFormat.toInstant(timeZone).toEpochMilliseconds())
|
||||
} else {
|
||||
formatDateWithMonth(dateToFormat)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRelativeDay(ts: Long): String {
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
ts,
|
||||
clock.now().toEpochMilliseconds(),
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
@@ -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.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.DaySeparatorFormatter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDaySeparatorFormatter @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : DaySeparatorFormatter {
|
||||
|
||||
override fun format(timestamp: Long): String {
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
return dateFormatters.formatDateWithYear(dateToFormat)
|
||||
}
|
||||
}
|
||||
@@ -16,91 +16,33 @@
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.LastMessageFormatter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.time.Period
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLastMessageFormatter @Inject constructor(
|
||||
private val clock: Clock,
|
||||
private val locale: Locale,
|
||||
private val timezone: TimeZone,
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : LastMessageFormatter {
|
||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
override fun format(timestamp: Long?): String {
|
||||
if (timestamp == null) return ""
|
||||
val now: Instant = clock.now()
|
||||
val tsInstant = Instant.fromEpochMilliseconds(timestamp)
|
||||
val nowDateTime = now.toLocalDateTime(timezone)
|
||||
val tsDateTime = tsInstant.toLocalDateTime(timezone)
|
||||
val isSameDay = nowDateTime.date == tsDateTime.date
|
||||
val currentDate = localDateTimeProvider.providesNow()
|
||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||
val isSameDay = currentDate.date == dateToFormat.date
|
||||
return when {
|
||||
isSameDay -> {
|
||||
onlyTimeFormatter.format(tsDateTime.toJavaLocalDateTime())
|
||||
dateFormatters.formatTime(dateToFormat)
|
||||
}
|
||||
else -> {
|
||||
formatDate(tsDateTime, nowDateTime)
|
||||
dateFormatters.formatDate(
|
||||
dateToFormat = dateToFormat,
|
||||
currentDate = currentDate,
|
||||
useRelative = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(
|
||||
date: LocalDateTime,
|
||||
currentDate: LocalDateTime,
|
||||
): String {
|
||||
val period = Period.between(date.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate())
|
||||
return if (period.years.absoluteValue >= 1) {
|
||||
formatDateWithYear(date)
|
||||
} else if (period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
|
||||
getRelativeDay(date.toInstant(timezone).toEpochMilliseconds())
|
||||
} else {
|
||||
formatDateWithMonth(date)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
private fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
private fun getRelativeDay(ts: Long): String {
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
ts,
|
||||
clock.now().toEpochMilliseconds(),
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.dateformatter.impl
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
class LocalDateTimeProvider @Inject constructor(
|
||||
private val clock: Clock,
|
||||
private val timezone: TimeZone,
|
||||
) {
|
||||
|
||||
fun providesNow(): LocalDateTime {
|
||||
val now: Instant = clock.now()
|
||||
return now.toLocalDateTime(timezone)
|
||||
}
|
||||
|
||||
fun providesFromTimestamp(timestamp: Long): LocalDateTime {
|
||||
val tsInstant = Instant.fromEpochMilliseconds(timestamp)
|
||||
return tsInstant.toLocalDateTime(timezone)
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,8 @@ class DefaultLastMessageFormatterTest {
|
||||
*/
|
||||
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageFormatter {
|
||||
val clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) }
|
||||
return DefaultLastMessageFormatter(clock, Locale.US, TimeZone.UTC)
|
||||
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
|
||||
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
|
||||
return DefaultLastMessageFormatter(localDateTimeProvider, dateFormatters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ anvil {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.rustsdk)
|
||||
// api(projects.libraries.rustsdk)
|
||||
api(libs.matrix.sdk)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
|
||||
@@ -36,13 +36,14 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncMode
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder
|
||||
import org.matrix.rustcomponents.sdk.StoppableSpawn
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
internal class RustMatrixClient internal constructor(
|
||||
class RustMatrixClient constructor(
|
||||
private val client: Client,
|
||||
private val sessionStore: SessionStore,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
@@ -66,25 +67,42 @@ internal class RustMatrixClient internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val slidingSyncView = SlidingSyncViewBuilder()
|
||||
.timelineLimit(limit = 10u)
|
||||
private val slidingSyncFilters by lazy {
|
||||
SlidingSyncRequestListFilters(
|
||||
isDm = null,
|
||||
spaces = emptyList(),
|
||||
isEncrypted = null,
|
||||
isInvite = false,
|
||||
isTombstoned = false,
|
||||
roomTypes = emptyList(),
|
||||
notRoomTypes = listOf("m.space"),
|
||||
roomNameLike = null,
|
||||
tags = emptyList(),
|
||||
notTags = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private val visibleRoomsView = SlidingSyncViewBuilder()
|
||||
.timelineLimit(limit = 1u)
|
||||
.requiredState(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.avatar", value = ""),
|
||||
RequiredState(key = "m.room.encryption", value = ""),
|
||||
)
|
||||
)
|
||||
.name(name = "HomeScreenView")
|
||||
.filters(slidingSyncFilters)
|
||||
.name(name = "CurrentlyVisibleRooms")
|
||||
.sendUpdatesForItems(true)
|
||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||
.addRange(0u, 30u)
|
||||
.addRange(0u, 20u)
|
||||
.build()
|
||||
|
||||
private val slidingSync = client
|
||||
.slidingSync()
|
||||
.homeserver("https://slidingsync.lab.element.dev")
|
||||
.homeserver("https://slidingsync.lab.matrix.org")
|
||||
.withCommonExtensions()
|
||||
// .coldCache("ElementX")
|
||||
.addView(slidingSyncView)
|
||||
.coldCache("ElementX")
|
||||
.addView(visibleRoomsView)
|
||||
.build()
|
||||
|
||||
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
|
||||
@@ -92,57 +110,58 @@ internal class RustMatrixClient internal constructor(
|
||||
RustRoomSummaryDataSource(
|
||||
slidingSyncObserverProxy.updateSummaryFlow,
|
||||
slidingSync,
|
||||
slidingSyncView,
|
||||
visibleRoomsView,
|
||||
dispatchers,
|
||||
::onRestartSync
|
||||
)
|
||||
private var slidingSyncObserverToken: StoppableSpawn? = null
|
||||
private var slidingSyncObserverToken: TaskHandle? = null
|
||||
|
||||
private val mediaResolver = RustMediaResolver(this)
|
||||
private val isSyncing = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
client.setDelegate(clientDelegate)
|
||||
roomSummaryDataSource.init()
|
||||
slidingSync.setObserver(slidingSyncObserverProxy)
|
||||
}
|
||||
|
||||
private fun onRestartSync() {
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
stopSync()
|
||||
startSync()
|
||||
}
|
||||
|
||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
|
||||
val room = slidingSyncRoom.fullRoom() ?: return null
|
||||
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
|
||||
return RustMatrixRoom(
|
||||
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
room = room,
|
||||
innerRoom = fullRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = dispatchers
|
||||
)
|
||||
}
|
||||
|
||||
override fun startSync() {
|
||||
if (isSyncing.compareAndSet(false, true)) {
|
||||
roomSummaryDataSource.startSync()
|
||||
slidingSync.setObserver(slidingSyncObserverProxy)
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopSync() {
|
||||
if (isSyncing.compareAndSet(true, false)) {
|
||||
roomSummaryDataSource.stopSync()
|
||||
slidingSync.setObserver(null)
|
||||
slidingSyncObserverToken?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
|
||||
|
||||
override fun mediaResolver(): MediaResolver = mediaResolver
|
||||
|
||||
override fun startSync() {
|
||||
if (client.isSoftLogout()) return
|
||||
if (isSyncing.compareAndSet(false, true)) {
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopSync() {
|
||||
if (isSyncing.compareAndSet(true, false)) {
|
||||
slidingSyncObserverToken?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
stopSync()
|
||||
slidingSync.setObserver(null)
|
||||
roomSummaryDataSource.close()
|
||||
client.setDelegate(null)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,6 @@ object MatrixModule {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesRustAuthenticationService(baseDirectory: File): AuthenticationService {
|
||||
return AuthenticationService(baseDirectory.absolutePath)
|
||||
return AuthenticationService(baseDirectory.absolutePath, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ interface MatrixRoom {
|
||||
|
||||
fun timeline(): MatrixTimeline
|
||||
|
||||
suspend fun fetchMembers(): Result<Unit>
|
||||
|
||||
suspend fun userDisplayName(userId: String): Result<String?>
|
||||
|
||||
suspend fun userAvatarUrl(userId: String): Result<String?>
|
||||
@@ -44,4 +46,5 @@ interface MatrixRoom {
|
||||
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit>
|
||||
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 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.room
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
|
||||
fun Room.timelineDiff(scope: CoroutineScope): Flow<TimelineDiff> = callbackFlow {
|
||||
val listener = object : TimelineListener {
|
||||
override fun onUpdate(update: TimelineDiff) {
|
||||
scope.launch {
|
||||
send(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
addTimelineListener(listener)
|
||||
awaitClose {
|
||||
removeTimeline()
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.sync.state
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -38,7 +37,6 @@ import org.matrix.rustcomponents.sdk.SlidingSyncViewRoomsListDiff
|
||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
|
||||
interface RoomSummaryDataSource {
|
||||
@@ -60,7 +58,7 @@ internal class RustRoomSummaryDataSource(
|
||||
private val roomSummaries = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
private val state = MutableStateFlow(SlidingSyncState.COLD)
|
||||
|
||||
fun startSync() {
|
||||
fun init() {
|
||||
coroutineScope.launch {
|
||||
updateRoomSummaries {
|
||||
addAll(
|
||||
@@ -89,10 +87,6 @@ internal class RustRoomSummaryDataSource(
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
fun stopSync() {
|
||||
coroutineScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
@@ -133,29 +127,45 @@ internal class RustRoomSummaryDataSource(
|
||||
}
|
||||
Timber.v("ApplyDiff: $diff for list with size: $size")
|
||||
when (diff) {
|
||||
is SlidingSyncViewRoomsListDiff.Push -> {
|
||||
is SlidingSyncViewRoomsListDiff.Append -> {
|
||||
val roomSummaries = diff.values.map {
|
||||
buildSummaryForRoomListEntry(it)
|
||||
}
|
||||
addAll(roomSummaries)
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.PushBack -> {
|
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
||||
add(roomSummary)
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.UpdateAt -> {
|
||||
is SlidingSyncViewRoomsListDiff.PushFront -> {
|
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
||||
add(0, roomSummary)
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.Set -> {
|
||||
fillUntil(diff.index.toInt())
|
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
||||
set(diff.index.toInt(), roomSummary)
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.InsertAt -> {
|
||||
is SlidingSyncViewRoomsListDiff.Insert -> {
|
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value)
|
||||
add(diff.index.toInt(), roomSummary)
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.Move -> {
|
||||
Collections.swap(this, diff.oldIndex.toInt(), diff.newIndex.toInt())
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.RemoveAt -> {
|
||||
is SlidingSyncViewRoomsListDiff.Remove -> {
|
||||
removeAt(diff.index.toInt())
|
||||
}
|
||||
is SlidingSyncViewRoomsListDiff.Replace -> {
|
||||
is SlidingSyncViewRoomsListDiff.Reset -> {
|
||||
clear()
|
||||
addAll(diff.values.map { buildSummaryForRoomListEntry(it) })
|
||||
}
|
||||
SlidingSyncViewRoomsListDiff.PopBack -> {
|
||||
removeFirstOrNull()
|
||||
}
|
||||
SlidingSyncViewRoomsListDiff.PopFront -> {
|
||||
removeLastOrNull()
|
||||
}
|
||||
SlidingSyncViewRoomsListDiff.Clear -> {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,13 +194,4 @@ internal class RustRoomSummaryDataSource(
|
||||
block(mutableRoomSummaries)
|
||||
roomSummaries.value = mutableRoomSummaries
|
||||
}
|
||||
|
||||
fun SlidingSyncViewRoomsListDiff.isInvalidation(): Boolean {
|
||||
return when (this) {
|
||||
is SlidingSyncViewRoomsListDiff.InsertAt -> this.value is RoomListEntry.Invalidated
|
||||
is SlidingSyncViewRoomsListDiff.UpdateAt -> this.value is RoomListEntry.Invalidated
|
||||
is SlidingSyncViewRoomsListDiff.Push -> this.value is RoomListEntry.Invalidated
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import timber.log.Timber
|
||||
|
||||
class RustMatrixRoom(
|
||||
private val slidingSyncUpdateFlow: Flow<UpdateSummary>,
|
||||
private val slidingSyncRoom: SlidingSyncRoom,
|
||||
private val room: Room,
|
||||
private val innerRoom: Room,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : MatrixRoom {
|
||||
@@ -44,7 +45,7 @@ class RustMatrixRoom(
|
||||
override fun syncUpdateFlow(): Flow<Long> {
|
||||
return slidingSyncUpdateFlow
|
||||
.filter {
|
||||
it.rooms.contains(room.id())
|
||||
it.rooms.contains(innerRoom.id())
|
||||
}
|
||||
.map {
|
||||
System.currentTimeMillis()
|
||||
@@ -55,14 +56,14 @@ class RustMatrixRoom(
|
||||
override fun timeline(): MatrixTimeline {
|
||||
return RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
room = room,
|
||||
innerRoom = innerRoom,
|
||||
slidingSyncRoom = slidingSyncRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
}
|
||||
|
||||
override val roomId = RoomId(room.id())
|
||||
override val roomId = RoomId(innerRoom.id())
|
||||
|
||||
override val name: String?
|
||||
get() {
|
||||
@@ -71,35 +72,41 @@ class RustMatrixRoom(
|
||||
|
||||
override val bestName: String
|
||||
get() {
|
||||
return name?.takeIf { it.isNotEmpty() } ?: room.id()
|
||||
return name?.takeIf { it.isNotEmpty() } ?: innerRoom.id()
|
||||
}
|
||||
|
||||
override val displayName: String
|
||||
get() {
|
||||
return room.displayName()
|
||||
return innerRoom.displayName()
|
||||
}
|
||||
|
||||
override val topic: String?
|
||||
get() {
|
||||
return room.topic()
|
||||
return innerRoom.topic()
|
||||
}
|
||||
|
||||
override val avatarUrl: String?
|
||||
get() {
|
||||
return room.avatarUrl()
|
||||
return innerRoom.avatarUrl()
|
||||
}
|
||||
|
||||
override suspend fun fetchMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: String): Result<String?> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
room.memberDisplayName(userId)
|
||||
innerRoom.memberDisplayName(userId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun userAvatarUrl(userId: String): Result<String?> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
room.memberAvatarUrl(userId)
|
||||
innerRoom.memberAvatarUrl(userId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +114,7 @@ class RustMatrixRoom(
|
||||
val transactionId = genTransactionId()
|
||||
val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
room.send(content, transactionId)
|
||||
innerRoom.send(content, transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +122,7 @@ class RustMatrixRoom(
|
||||
val transactionId = genTransactionId()
|
||||
// val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
room.edit(/* TODO use content */ message, originalEventId.value, transactionId)
|
||||
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,14 +130,14 @@ class RustMatrixRoom(
|
||||
val transactionId = genTransactionId()
|
||||
// val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
room.sendReply(/* TODO use content */ message, eventId.value, transactionId)
|
||||
innerRoom.sendReply(/* TODO use content */ message, eventId.value, transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
runCatching {
|
||||
room.redact(eventId.value, reason, transactionId)
|
||||
innerRoom.redact(eventId.value, reason, transactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ class PreferencesSessionStore @Inject constructor(
|
||||
val homeserverUrl: String,
|
||||
val isSoftLogout: Boolean,
|
||||
val refreshToken: String?,
|
||||
val userId: String
|
||||
val userId: String,
|
||||
val slidingSyncProxy: String?
|
||||
)
|
||||
|
||||
private val store = context.dataStore
|
||||
@@ -73,7 +74,8 @@ class PreferencesSessionStore @Inject constructor(
|
||||
homeserverUrl = session.homeserverUrl,
|
||||
isSoftLogout = session.isSoftLogout,
|
||||
refreshToken = session.refreshToken,
|
||||
userId = session.userId
|
||||
userId = session.userId,
|
||||
slidingSyncProxy = session.slidingSyncProxy
|
||||
)
|
||||
val encodedSession = Json.encodeToString(sessionData)
|
||||
prefs[sessionKey] = encodedSession
|
||||
@@ -90,7 +92,8 @@ class PreferencesSessionStore @Inject constructor(
|
||||
homeserverUrl = sessionData.homeserverUrl,
|
||||
isSoftLogout = sessionData.isSoftLogout,
|
||||
refreshToken = sessionData.refreshToken,
|
||||
userId = sessionData.userId
|
||||
userId = sessionData.userId,
|
||||
slidingSyncProxy = sessionData.slidingSyncProxy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,19 @@ package io.element.android.libraries.matrix.timeline
|
||||
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface MatrixTimeline {
|
||||
var callback: Callback?
|
||||
val hasMoreToLoad: Boolean
|
||||
|
||||
interface Callback {
|
||||
fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
||||
fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
||||
}
|
||||
data class PaginationState(
|
||||
val isBackPaginating: Boolean,
|
||||
val canBackPaginate: Boolean
|
||||
)
|
||||
|
||||
fun paginationState(): StateFlow<PaginationState>
|
||||
|
||||
fun timelineItems(): Flow<List<MatrixTimelineItem>>
|
||||
suspend fun paginateBackwards(count: Int): Result<Unit>
|
||||
fun addListener(timelineListener: TimelineListener)
|
||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||
fun initialize()
|
||||
fun dispose()
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.timeline
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.TimelineChange
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
|
||||
|
||||
internal class MatrixTimelineDiffProcessor(
|
||||
private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState>,
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val diffDispatcher: CoroutineDispatcher,
|
||||
) : TimelineListener {
|
||||
|
||||
override fun onUpdate(update: TimelineDiff) {
|
||||
coroutineScope.launch {
|
||||
updateTimelineItems {
|
||||
applyDiff(update)
|
||||
}
|
||||
when (val firstItem = timelineItems.value.firstOrNull()) {
|
||||
is MatrixTimelineItem.Virtual -> updateBackPaginationState(firstItem.virtual)
|
||||
else -> updateBackPaginationState(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBackPaginationState(virtualItem: VirtualTimelineItem?) {
|
||||
val currentPaginationState = paginationState.value
|
||||
val newPaginationState = when (virtualItem) {
|
||||
VirtualTimelineItem.LoadingIndicator -> currentPaginationState.copy(
|
||||
isBackPaginating = true,
|
||||
canBackPaginate = true
|
||||
)
|
||||
VirtualTimelineItem.TimelineStart -> currentPaginationState.copy(
|
||||
isBackPaginating = false,
|
||||
canBackPaginate = false
|
||||
)
|
||||
else -> currentPaginationState.copy(
|
||||
isBackPaginating = false,
|
||||
canBackPaginate = true
|
||||
)
|
||||
}
|
||||
paginationState.value = newPaginationState
|
||||
}
|
||||
|
||||
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
|
||||
withContext(diffDispatcher) {
|
||||
val mutableTimelineItems = timelineItems.value.toMutableList()
|
||||
block(mutableTimelineItems)
|
||||
timelineItems.value = mutableTimelineItems
|
||||
}
|
||||
|
||||
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
|
||||
when (diff.change()) {
|
||||
TimelineChange.APPEND -> {
|
||||
val items = diff.append()?.map { it.asMatrixTimelineItem() } ?: return
|
||||
addAll(items)
|
||||
}
|
||||
TimelineChange.PUSH_BACK -> {
|
||||
val item = diff.pushBack()?.asMatrixTimelineItem() ?: return
|
||||
add(item)
|
||||
}
|
||||
TimelineChange.PUSH_FRONT -> {
|
||||
val item = diff.pushFront()?.asMatrixTimelineItem() ?: return
|
||||
add(0, item)
|
||||
}
|
||||
TimelineChange.SET -> {
|
||||
val updateAtData = diff.set() ?: return
|
||||
val item = updateAtData.item.asMatrixTimelineItem()
|
||||
set(updateAtData.index.toInt(), item)
|
||||
}
|
||||
TimelineChange.INSERT -> {
|
||||
val insertAtData = diff.insert() ?: return
|
||||
val item = insertAtData.item.asMatrixTimelineItem()
|
||||
add(insertAtData.index.toInt(), item)
|
||||
}
|
||||
TimelineChange.REMOVE -> {
|
||||
val removeAtData = diff.remove() ?: return
|
||||
removeAt(removeAtData.toInt())
|
||||
}
|
||||
TimelineChange.RESET -> {
|
||||
clear()
|
||||
val items = diff.reset()?.map { it.asMatrixTimelineItem() } ?: return
|
||||
addAll(items)
|
||||
}
|
||||
TimelineChange.POP_FRONT -> {
|
||||
removeFirstOrNull()
|
||||
}
|
||||
TimelineChange.POP_BACK -> {
|
||||
removeLastOrNull()
|
||||
}
|
||||
TimelineChange.CLEAR -> {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,18 @@
|
||||
|
||||
package io.element.android.libraries.matrix.timeline
|
||||
|
||||
import io.element.android.libraries.matrix.core.EventId
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineKey
|
||||
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
|
||||
|
||||
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()
|
||||
val eventId: EventId? = event.eventId()?.let { EventId(it) }
|
||||
}
|
||||
|
||||
object Virtual : MatrixTimelineItem
|
||||
data class Virtual(val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
||||
object Other : MatrixTimelineItem
|
||||
}
|
||||
|
||||
@@ -40,7 +38,7 @@ fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem {
|
||||
}
|
||||
val asVirtual = asVirtual()
|
||||
if (asVirtual != null) {
|
||||
return MatrixTimelineItem.Virtual
|
||||
return MatrixTimelineItem.Virtual(asVirtual)
|
||||
}
|
||||
return MatrixTimelineItem.Other
|
||||
}
|
||||
|
||||
@@ -18,121 +18,83 @@ 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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.util.TaskHandleBag
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
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.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
||||
class RustMatrixTimeline(
|
||||
private val matrixRoom: RustMatrixRoom,
|
||||
private val room: Room,
|
||||
private val matrixRoom: MatrixRoom,
|
||||
private val innerRoom: Room,
|
||||
private val slidingSyncRoom: SlidingSyncRoom,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : TimelineListener, MatrixTimeline {
|
||||
) : MatrixTimeline {
|
||||
|
||||
override var callback: MatrixTimeline.Callback? = null
|
||||
|
||||
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
||||
MutableStateFlow(emptyList())
|
||||
|
||||
private val paginationState = MutableStateFlow(
|
||||
MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false)
|
||||
)
|
||||
|
||||
private val innerTimelineListener = MatrixTimelineDiffProcessor(
|
||||
paginationState = paginationState,
|
||||
timelineItems = timelineItems,
|
||||
coroutineScope = coroutineScope,
|
||||
diffDispatcher = coroutineDispatchers.diffUpdateDispatcher
|
||||
)
|
||||
|
||||
private val listenerTokens = TaskHandleBag()
|
||||
override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> {
|
||||
return paginationState
|
||||
}
|
||||
|
||||
@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 -> {
|
||||
Timber.v("Apply push on list with size: $size")
|
||||
val item = diff.push()?.asMatrixTimelineItem() ?: return
|
||||
callback?.onPushedTimelineItem(item)
|
||||
add(item)
|
||||
}
|
||||
TimelineChange.UPDATE_AT -> {
|
||||
val updateAtData = diff.updateAt() ?: return
|
||||
Timber.v("Apply $updateAtData on list with size: $size")
|
||||
val item = updateAtData.item.asMatrixTimelineItem()
|
||||
callback?.onUpdatedTimelineItem(item)
|
||||
set(updateAtData.index.toInt(), item)
|
||||
}
|
||||
TimelineChange.INSERT_AT -> {
|
||||
val insertAtData = diff.insertAt() ?: return
|
||||
Timber.v("Apply $insertAtData on list with size: $size")
|
||||
val item = insertAtData.item.asMatrixTimelineItem()
|
||||
add(insertAtData.index.toInt(), item)
|
||||
}
|
||||
TimelineChange.MOVE -> {
|
||||
val moveData = diff.move() ?: return
|
||||
Timber.v("Apply $moveData on list with size: $size")
|
||||
Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt())
|
||||
}
|
||||
TimelineChange.REMOVE_AT -> {
|
||||
val removeAtData = diff.removeAt() ?: return
|
||||
Timber.v("Apply $removeAtData on list with size: $size")
|
||||
removeAt(removeAtData.toInt())
|
||||
}
|
||||
TimelineChange.REPLACE -> {
|
||||
Timber.v("Apply REPLACE on list with size: $size")
|
||||
clear()
|
||||
val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return
|
||||
addAll(items)
|
||||
}
|
||||
TimelineChange.POP -> {
|
||||
Timber.v("Apply POP on list with size: $size")
|
||||
removeLast()
|
||||
}
|
||||
TimelineChange.CLEAR -> {
|
||||
Timber.v("Apply CLEAR on list with size: $size")
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun paginateBackwards(count: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
if (!paginationOutcome.value.moreMessages) {
|
||||
return@withContext Result.failure(IllegalStateException("no more message"))
|
||||
}
|
||||
runCatching {
|
||||
paginationOutcome.value = room.paginateBackwards(count.toUShort())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
|
||||
withContext(coroutineDispatchers.diffUpdateDispatcher) {
|
||||
val mutableTimelineItems = timelineItems.value.toMutableList()
|
||||
block(mutableTimelineItems)
|
||||
timelineItems.value = mutableTimelineItems
|
||||
}
|
||||
|
||||
override fun addListener(timelineListener: TimelineListener) {
|
||||
slidingSyncRoom.addTimelineListener(timelineListener)
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
addListener(this)
|
||||
Timber.v("Init timeline for room ${matrixRoom.roomId}")
|
||||
coroutineScope.launch {
|
||||
matrixRoom.fetchMembers()
|
||||
.onFailure {
|
||||
Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}")
|
||||
}.onSuccess {
|
||||
Timber.v("Success fetching members for room ${matrixRoom.roomId}")
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
val result = addListener(innerTimelineListener)
|
||||
result
|
||||
.onSuccess { timelineItems ->
|
||||
val matrixTimelineItems = timelineItems.map { it.asMatrixTimelineItem() }
|
||||
withContext(coroutineDispatchers.diffUpdateDispatcher) {
|
||||
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
slidingSyncRoom.removeTimeline()
|
||||
Timber.v("Dispose timeline for room ${matrixRoom.roomId}")
|
||||
listenerTokens.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,11 +112,26 @@ class RustMatrixTimeline(
|
||||
return matrixRoom.replyMessage(inReplyToEventId, message)
|
||||
}
|
||||
|
||||
override fun onUpdate(update: TimelineDiff) {
|
||||
coroutineScope.launch {
|
||||
updateTimelineItems {
|
||||
applyDiff(update)
|
||||
}
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
||||
eventLimit = requestSize.toUShort(),
|
||||
items = untilNumberOfItems.toUShort()
|
||||
)
|
||||
innerRoom.paginateBackwards(paginationOptions)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Fail to paginate for room ${matrixRoom.roomId}")
|
||||
}.onSuccess {
|
||||
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null)
|
||||
listenerTokens += result.taskHandle
|
||||
result.items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,49 +16,73 @@
|
||||
|
||||
package io.element.android.libraries.matrix.tracing
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
data class TracingConfiguration(
|
||||
val common: LogLevel = LogLevel.Warn,
|
||||
val targets: Map<Target, LogLevel> = emptyMap()
|
||||
val overrides: Map<Target, LogLevel> = emptyMap()
|
||||
) {
|
||||
|
||||
val filter = "$common,${
|
||||
targets.map { "${it.key.filter}=${it.value.filter}" }.joinToString(separator = ",")
|
||||
}"
|
||||
// Order should matters
|
||||
private val targets: MutableMap<Target, LogLevel> = mutableMapOf(
|
||||
Target.Common to LogLevel.Warn,
|
||||
Target.Hyper to LogLevel.Warn,
|
||||
Target.Sled to LogLevel.Warn,
|
||||
Target.MatrixSdk.Root to LogLevel.Warn,
|
||||
Target.MatrixSdk.Sled to LogLevel.Warn,
|
||||
Target.MatrixSdk.Crypto to LogLevel.Debug,
|
||||
Target.MatrixSdk.HttpClient to LogLevel.Debug,
|
||||
Target.MatrixSdk.SlidingSync to LogLevel.Trace,
|
||||
Target.MatrixSdk.BaseSlidingSync to LogLevel.Trace,
|
||||
)
|
||||
|
||||
sealed class Target(open val filter: String) {
|
||||
object Hyper : Target("hyper")
|
||||
object Sled : Target("sled")
|
||||
sealed class MatrixSdk(override val filter: String) : Target(filter) {
|
||||
object Root : MatrixSdk("matrix_sdk")
|
||||
object Sled : MatrixSdk("matrix_sdk_sled")
|
||||
object FFI : MatrixSdk("matrix_sdk_ffi")
|
||||
object HttpClient : MatrixSdk("matrix_sdk::http_client")
|
||||
object UniffiAPI : MatrixSdk("matrix_sdk_ffi::uniffi_api")
|
||||
object SlidingSync : MatrixSdk("matrix_sdk::sliding_sync")
|
||||
object BaseSlidingSync : MatrixSdk("matrix_sdk_base::sliding_sync")
|
||||
val filter: String
|
||||
get() {
|
||||
overrides.forEach { (target, logLevel) ->
|
||||
targets[target] = logLevel
|
||||
}
|
||||
return targets.map {
|
||||
if (it.key.filter.isEmpty()) {
|
||||
it.value.filter
|
||||
} else {
|
||||
"${it.key.filter}=${it.value.filter}"
|
||||
}
|
||||
}.joinToString(separator = ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LogLevel(val filter: String) {
|
||||
object Warn : LogLevel("warn")
|
||||
object Trace : LogLevel("trace")
|
||||
object Info : LogLevel("info")
|
||||
object Debug : LogLevel("debug")
|
||||
object Error : LogLevel("error")
|
||||
sealed class Target(open val filter: String) {
|
||||
object Common : Target("")
|
||||
object Hyper : Target("hyper")
|
||||
object Sled : Target("sled")
|
||||
sealed class MatrixSdk(override val filter: String) : Target(filter) {
|
||||
object Root : MatrixSdk("matrix_sdk")
|
||||
object Sled : MatrixSdk("matrix_sdk_sled")
|
||||
object Crypto: MatrixSdk("matrix_sdk_crypto")
|
||||
object FFI : MatrixSdk("matrix_sdk_ffi")
|
||||
object HttpClient : MatrixSdk("matrix_sdk::http_client")
|
||||
object UniffiAPI : MatrixSdk("matrix_sdk_ffi::uniffi_api")
|
||||
object SlidingSync : MatrixSdk("matrix_sdk::sliding_sync")
|
||||
object BaseSlidingSync : MatrixSdk("matrix_sdk_base::sliding_sync")
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LogLevel(val filter: String) {
|
||||
object Warn : LogLevel("warn")
|
||||
object Trace : LogLevel("trace")
|
||||
object Info : LogLevel("info")
|
||||
object Debug : LogLevel("debug")
|
||||
object Error : LogLevel("error")
|
||||
}
|
||||
|
||||
fun setupTracing(tracingConfiguration: TracingConfiguration) {
|
||||
org.matrix.rustcomponents.sdk.setupTracing(tracingConfiguration.filter)
|
||||
val filter = tracingConfiguration.filter
|
||||
Timber.v("Tracing config filter = $filter")
|
||||
org.matrix.rustcomponents.sdk.setupTracing(filter)
|
||||
}
|
||||
|
||||
object TracingConfigurations {
|
||||
val release = TracingConfiguration(common = TracingConfiguration.LogLevel.Info)
|
||||
val debug = TracingConfiguration()
|
||||
val full = TracingConfiguration(
|
||||
common = TracingConfiguration.LogLevel.Info,
|
||||
targets = mapOf(
|
||||
TracingConfiguration.Target.Sled to TracingConfiguration.LogLevel.Warn
|
||||
)
|
||||
)
|
||||
val release = TracingConfiguration(overrides = mapOf(Target.Common to LogLevel.Info))
|
||||
val debug = TracingConfiguration(overrides = mapOf(Target.Common to LogLevel.Info))
|
||||
|
||||
fun custom(overrides: Map<Target, LogLevel>) = TracingConfiguration(overrides)
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ package io.element.android.libraries.matrix.util
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.matrix.rustcomponents.sdk.StoppableSpawn
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
|
||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> StoppableSpawn) =
|
||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle) =
|
||||
callbackFlow {
|
||||
val token: StoppableSpawn = block(this)
|
||||
val token: TaskHandle = block(this)
|
||||
awaitClose {
|
||||
token.cancel()
|
||||
token.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.TaskHandle
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class TaskHandleBag(private val tokens: MutableSet<TaskHandle> = CopyOnWriteArraySet()) : Set<TaskHandle> by tokens {
|
||||
|
||||
operator fun plusAssign(taskHandle: TaskHandle?) {
|
||||
if (taskHandle == null) return
|
||||
tokens += taskHandle
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
tokens.forEach { it.cancel() }
|
||||
tokens.clear()
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,10 @@ class FakeMatrixRoom(
|
||||
return matrixTimeline
|
||||
}
|
||||
|
||||
override suspend fun fetchMembers(): Result<Unit> {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: String): Result<String?> {
|
||||
return Result.success("")
|
||||
}
|
||||
|
||||
@@ -21,35 +21,55 @@ import io.element.android.libraries.matrix.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeMatrixTimeline : MatrixTimeline {
|
||||
override var callback: MatrixTimeline.Callback? = null
|
||||
class FakeMatrixTimeline(
|
||||
initialTimelineItems: List<MatrixTimelineItem> = emptyList(),
|
||||
initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false)
|
||||
) : MatrixTimeline {
|
||||
|
||||
private var hasMoreToLoadValue: Boolean = true
|
||||
private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
|
||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
|
||||
var isInitialized = false
|
||||
|
||||
fun givenHasMoreToLoad(hasMoreToLoad: Boolean) {
|
||||
this.hasMoreToLoadValue = hasMoreToLoad
|
||||
fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) {
|
||||
paginationState.value = update(paginationState.value)
|
||||
}
|
||||
|
||||
override val hasMoreToLoad: Boolean
|
||||
get() = hasMoreToLoadValue
|
||||
fun updateTimelineItems(update: (items: List<MatrixTimelineItem>) -> List<MatrixTimelineItem>) {
|
||||
timelineItems.value = update(timelineItems.value)
|
||||
}
|
||||
|
||||
override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> {
|
||||
return paginationState
|
||||
}
|
||||
|
||||
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||
return emptyFlow()
|
||||
return timelineItems
|
||||
}
|
||||
|
||||
override suspend fun paginateBackwards(count: Int): Result<Unit> {
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
||||
updatePaginationState {
|
||||
copy(isBackPaginating = true)
|
||||
}
|
||||
delay(100)
|
||||
updatePaginationState {
|
||||
copy(isBackPaginating = false)
|
||||
}
|
||||
updateTimelineItems { timelineItems ->
|
||||
timelineItems
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override fun addListener(timelineListener: TimelineListener) = Unit
|
||||
override fun initialize() {
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
override fun dispose() = Unit
|
||||
override fun dispose() {
|
||||
isInitialized = false
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||
return Result.success(Unit)
|
||||
|
||||
@@ -28,4 +28,5 @@ dependencies {
|
||||
implementation(libs.android.gradle.plugin)
|
||||
implementation(libs.kotlin.gradle.plugin)
|
||||
implementation(libs.firebase.gradle.plugin)
|
||||
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ package extension
|
||||
|
||||
import Versions
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
fun CommonExtension<*, *, *, *>.androidConfig(project: Project) {
|
||||
defaultConfig {
|
||||
@@ -30,6 +32,8 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions {
|
||||
@@ -44,13 +48,14 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) {
|
||||
}
|
||||
}
|
||||
|
||||
fun CommonExtension<*, *, *, *>.composeConfig() {
|
||||
fun CommonExtension<*, *, *, *>.composeConfig(libs: LibrariesForLibs) {
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.2" // libs.versions.composecompiler.get()
|
||||
kotlinCompilerExtensionVersion = libs.versions.composecompiler.get()
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user