Finish migration of Messages screen

This commit is contained in:
ganfra 2023-01-13 18:05:14 +01:00
parent 2869f492d9
commit 7b197e6e8b
47 changed files with 354 additions and 315 deletions

View file

@ -19,10 +19,14 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import io.element.android.x.core.coroutine.CoroutineDispatchers
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import java.util.concurrent.Executors
@Module @Module
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@ -33,4 +37,15 @@ object AppModule {
fun providesAppCoroutineScope(): CoroutineScope { fun providesAppCoroutineScope(): CoroutineScope {
return MainScope() + CoroutineName("ElementX Scope") return MainScope() + CoroutineName("ElementX Scope")
} }
@Provides
@SingleIn(AppScope::class)
fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
}
} }

View file

@ -1,8 +1,8 @@
package io.element.android.x.features.messages package io.element.android.x.features.messages
import io.element.android.x.features.messages.actionlist.TimelineItemAction import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.timeline.model.TimelineItem
sealed interface MessagesEvents { sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val messageEvent: MessagesTimelineItemState.MessageEvent) : MessagesEvents data class HandleAction(val action: TimelineItemAction, val messageEvent: TimelineItem.MessageEvent) : MessagesEvents
} }

View file

@ -28,6 +28,7 @@ class MessagesNode @AssistedInject constructor(
MessagesView( MessagesView(
state = state, state = state,
onBackPressed = this::navigateUp, onBackPressed = this::navigateUp,
modifier = modifier
) )
} }
} }

View file

@ -12,13 +12,14 @@ import io.element.android.x.architecture.Presenter
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.actionlist.ActionListPresenter import io.element.android.x.features.messages.actionlist.ActionListPresenter
import io.element.android.x.features.messages.actionlist.TimelineItemAction import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.features.messages.textcomposer.MessageComposerEvents import io.element.android.x.features.messages.textcomposer.MessageComposerEvents
import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter import io.element.android.x.features.messages.textcomposer.MessageComposerPresenter
import io.element.android.x.features.messages.textcomposer.MessageComposerState import io.element.android.x.features.messages.textcomposer.MessageComposerState
import io.element.android.x.features.messages.timeline.TimelineEvents
import io.element.android.x.features.messages.timeline.TimelinePresenter import io.element.android.x.features.messages.timeline.TimelinePresenter
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.ui.MatrixItemHelper import io.element.android.x.matrix.ui.MatrixItemHelper
@ -60,7 +61,9 @@ class MessagesPresenter @Inject constructor(
) )
roomName.value = room.name roomName.value = room.name
} }
LaunchedEffect(composerState.mode.relatedEventId) {
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
fun handleEvents(event: MessagesEvents) { fun handleEvents(event: MessagesEvents) {
when (event) { when (event) {
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState) is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState)
@ -79,7 +82,7 @@ class MessagesPresenter @Inject constructor(
fun CoroutineScope.handleTimelineAction( fun CoroutineScope.handleTimelineAction(
action: TimelineItemAction, action: TimelineItemAction,
targetEvent: MessagesTimelineItemState.MessageEvent, targetEvent: TimelineItem.MessageEvent,
composerState: MessageComposerState, composerState: MessageComposerState,
) = launch { ) = launch {
when (action) { when (action) {
@ -95,21 +98,21 @@ class MessagesPresenter @Inject constructor(
Timber.v("NotImplementedYet") Timber.v("NotImplementedYet")
} }
private suspend fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) { private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) {
room.redactEvent(event.id) room.redactEvent(event.id)
} }
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Edit( val composerMode = MessageComposerMode.Edit(
targetEvent.id, targetEvent.id,
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty() (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty()
) )
composerState.eventSink( composerState.eventSink(
MessageComposerEvents.SetMode(composerMode) MessageComposerEvents.SetMode(composerMode)
) )
} }
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) { private fun handleActionReply(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "") val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, "")
composerState.eventSink( composerState.eventSink(
MessageComposerEvents.SetMode(composerMode) MessageComposerEvents.SetMode(composerMode)

View file

@ -10,10 +10,10 @@ import io.element.android.x.matrix.core.RoomId
@Immutable @Immutable
data class MessagesState( data class MessagesState(
val roomId: RoomId, val roomId: RoomId,
val roomName: String? = null, val roomName: String?,
val roomAvatar: AvatarData? = null, val roomAvatar: AvatarData?,
val composerState: MessageComposerState, val composerState: MessageComposerState,
val timelineState: TimelineState, val timelineState: TimelineState,
val actionListState: ActionListState, val actionListState: ActionListState,
val eventSink: (MessagesEvents) -> Unit = {} val eventSink: (MessagesEvents) -> Unit
) )

View file

@ -48,8 +48,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -57,11 +59,13 @@ import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.components.avatar.Avatar import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.actionlist.TimelineItemAction import io.element.android.x.features.messages.actionlist.ActionListEvents
import io.element.android.x.features.messages.actionlist.ActionListView import io.element.android.x.features.messages.actionlist.ActionListView
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.textcomposer.MessageComposerView import io.element.android.x.features.messages.textcomposer.MessageComposerView
import io.element.android.x.features.messages.timeline.TimelineView import io.element.android.x.features.messages.timeline.TimelineView
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@Composable @Composable
@ -76,8 +80,28 @@ fun MessagesView(
initialValue = ModalBottomSheetValue.Hidden, initialValue = ModalBottomSheetValue.Hidden,
) )
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
LogCompositions(tag = "MessagesScreen", msg = "Content") LogCompositions(tag = "MessagesScreen", msg = "Content")
fun onMessageClicked(messageEvent: TimelineItem.MessageEvent) {
Timber.v("OnMessageClicked= ${messageEvent.id}")
}
fun onMessageLongClicked(messageEvent: TimelineItem.MessageEvent) {
Timber.v("OnMessageLongClicked= ${messageEvent.id}")
focusManager.clearFocus(force = true)
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(messageEvent))
coroutineScope.launch {
itemActionsBottomSheetState.show()
}
}
fun onActionSelected(action: TimelineItemAction, messageEvent: TimelineItem.MessageEvent) {
state.eventSink(MessagesEvents.HandleAction(action, messageEvent))
}
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
@ -92,6 +116,8 @@ fun MessagesView(
MessagesViewContent( MessagesViewContent(
state = state, state = state,
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked
) )
}, },
snackbarHost = { snackbarHost = {
@ -102,10 +128,6 @@ fun MessagesView(
}, },
) )
fun onActionSelected(action: TimelineItemAction, messageEvent: MessagesTimelineItemState.MessageEvent) {
state.eventSink(MessagesEvents.HandleAction(action, messageEvent))
}
ActionListView( ActionListView(
state = state.actionListState, state = state.actionListState,
modalBottomSheetState = itemActionsBottomSheetState, modalBottomSheetState = itemActionsBottomSheetState,
@ -116,43 +138,32 @@ fun MessagesView(
@Composable @Composable
fun MessagesViewContent( fun MessagesViewContent(
state: MessagesState, state: MessagesState,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {},
onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {},
) { ) {
fun onMessageClicked(messageEvent: MessagesTimelineItemState.MessageEvent) {
Timber.v("OnMessageClicked= $messageEvent")
}
fun onMessageLongClicked(messageEvent: MessagesTimelineItemState.MessageEvent) {
Timber.v("OnMessageLongClicked= $messageEvent")
}
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
) { ) {
// Hide timeline if composer is full screen
if (!state.composerState.isFullScreen) { if (!state.composerState.isFullScreen) {
TimelineView( TimelineView(
state = state.timelineState, state = state.timelineState,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(1f),
onMessageClicked = ::onMessageClicked, onMessageClicked = onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked onMessageLongClicked = onMessageLongClicked
) )
} }
MessageComposerView( MessageComposerView(
state = state.composerState, state = state.composerState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.let { .wrapContentHeight(Alignment.Bottom)
if (state.composerState.isFullScreen) {
it.weight(1f, fill = false)
} else {
it.wrapContentHeight(Alignment.Bottom)
}
},
) )
} }
} }

View file

@ -1,8 +1,8 @@
package io.element.android.x.features.messages.actionlist package io.element.android.x.features.messages.actionlist
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.timeline.model.TimelineItem
sealed interface ActionListEvents { sealed interface ActionListEvents {
object Clear : ActionListEvents object Clear : ActionListEvents
data class ComputeForMessage(val messageEvent: MessagesTimelineItemState.MessageEvent) : ActionListEvents data class ComputeForMessage(val messageEvent: TimelineItem.MessageEvent) : ActionListEvents
} }

View file

@ -6,8 +6,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,10 +38,10 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
) )
} }
fun CoroutineScope.computeForMessage(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent, target: MutableState<ActionListState.Target>) = launch { fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.MessageEvent, target: MutableState<ActionListState.Target>) = launch {
target.value = ActionListState.Target.Loading(messagesTimelineItemState) target.value = ActionListState.Target.Loading(timelineItem)
val actions = val actions =
if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) { if (timelineItem.content is TimelineItemRedactedContent) {
emptyList() emptyList()
} else { } else {
mutableListOf( mutableListOf(
@ -48,12 +49,12 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
TimelineItemAction.Forward, TimelineItemAction.Forward,
TimelineItemAction.Copy, TimelineItemAction.Copy,
).also { ).also {
if (messagesTimelineItemState.isMine) { if (timelineItem.isMine) {
it.add(TimelineItemAction.Edit) it.add(TimelineItemAction.Edit)
it.add(TimelineItemAction.Redact) it.add(TimelineItemAction.Redact)
} }
} }
} }
target.value = ActionListState.Target.Success(messagesTimelineItemState, actions.toImmutableList()) target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList())
} }
} }

View file

@ -17,20 +17,21 @@
package io.element.android.x.features.messages.actionlist package io.element.android.x.features.messages.actionlist
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
data class ActionListState( data class ActionListState(
val target: Target = Target.None, val target: Target,
val eventSink: (ActionListEvents) -> Unit = {}, val eventSink: (ActionListEvents) -> Unit,
) { ) {
sealed interface Target { sealed interface Target {
object None : Target object None : Target
data class Loading(val messageEvent: MessagesTimelineItemState.MessageEvent) : Target data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target
data class Success( data class Success(
val messageEvent: MessagesTimelineItemState.MessageEvent, val messageEvent: TimelineItem.MessageEvent,
val actions: ImmutableList<TimelineItemAction>, val actions: ImmutableList<TimelineItemAction>,
) : Target ) : Target
} }

View file

@ -26,7 +26,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.x.designsystem.components.VectorIcon import io.element.android.x.designsystem.components.VectorIcon
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
import io.element.android.x.features.messages.timeline.model.TimelineItem
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -34,7 +35,7 @@ import kotlinx.coroutines.launch
fun ActionListView( fun ActionListView(
state: ActionListState, state: ActionListState,
modalBottomSheetState: ModalBottomSheetState, modalBottomSheetState: ModalBottomSheetState,
onActionSelected: (action: TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit, onActionSelected: (action: TimelineItemAction, TimelineItem.MessageEvent) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -48,7 +49,7 @@ fun ActionListView(
fun onItemActionClicked( fun onItemActionClicked(
itemAction: TimelineItemAction, itemAction: TimelineItemAction,
targetItem: MessagesTimelineItemState.MessageEvent targetItem: TimelineItem.MessageEvent
) { ) {
onActionSelected(itemAction, targetItem) onActionSelected(itemAction, targetItem)
coroutineScope.launch { coroutineScope.launch {
@ -75,7 +76,7 @@ fun ActionListView(
private fun SheetContent( private fun SheetContent(
state: ActionListState, state: ActionListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onActionClicked: (TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> }, onActionClicked: (TimelineItemAction, TimelineItem.MessageEvent) -> Unit = { _, _ -> },
) { ) {
when (val target = state.target) { when (val target = state.target) {
is ActionListState.Target.Loading, is ActionListState.Target.Loading,

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.actionlist package io.element.android.x.features.messages.actionlist.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable

View file

@ -1,12 +1,13 @@
package io.element.android.x.features.messages.textcomposer package io.element.android.x.features.messages.textcomposer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.core.data.toStableCharSequence import io.element.android.x.core.data.toStableCharSequence
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.textcomposer.MessageComposerMode import io.element.android.x.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -15,7 +16,6 @@ import javax.inject.Inject
class MessageComposerPresenter @Inject constructor( class MessageComposerPresenter @Inject constructor(
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val client: MatrixClient,
private val room: MatrixRoom private val room: MatrixRoom
) : Presenter<MessageComposerState> { ) : Presenter<MessageComposerState> {
@ -24,25 +24,32 @@ class MessageComposerPresenter @Inject constructor(
val isFullScreen = rememberSaveable { val isFullScreen = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
val text: MutableState<CharSequence> = rememberSaveable { val text: MutableState<StableCharSequence> = rememberSaveable {
mutableStateOf("") mutableStateOf(StableCharSequence(""))
} }
val composerMode: MutableState<MessageComposerMode> = rememberSaveable { val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
mutableStateOf(MessageComposerMode.Normal("")) mutableStateOf(MessageComposerMode.Normal(""))
} }
LaunchedEffect(composerMode.value) {
when (val modeValue = composerMode.value) {
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence()
else -> Unit
}
}
fun handleEvents(event: MessageComposerEvents) { fun handleEvents(event: MessageComposerEvents) {
when (event) { when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
is MessageComposerEvents.UpdateText -> text.value = event.text is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence()
MessageComposerEvents.CloseSpecialMode -> composerMode.setToNormal() MessageComposerEvents.CloseSpecialMode -> composerMode.setToNormal()
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode) is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode
} }
} }
return MessageComposerState( return MessageComposerState(
text = text.value.toStableCharSequence(), text = text.value,
isFullScreen = isFullScreen.value, isFullScreen = isFullScreen.value,
mode = composerMode.value, mode = composerMode.value,
eventSink = ::handleEvents eventSink = ::handleEvents
@ -53,9 +60,10 @@ class MessageComposerPresenter @Inject constructor(
value = MessageComposerMode.Normal("") value = MessageComposerMode.Normal("")
} }
private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>) = launch { private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>, textState: MutableState<StableCharSequence>) = launch {
val capturedMode = composerMode.value val capturedMode = composerMode.value
// Reset composer right away // Reset composer right away
textState.value = "".toStableCharSequence()
composerMode.setToNormal() composerMode.setToNormal()
when (capturedMode) { when (capturedMode) {
is MessageComposerMode.Normal -> room.sendMessage(text) is MessageComposerMode.Normal -> room.sendMessage(text)

View file

@ -22,16 +22,10 @@ import io.element.android.x.textcomposer.MessageComposerMode
@Immutable @Immutable
data class MessageComposerState( data class MessageComposerState(
// val roomId: String, val text: StableCharSequence?,
// val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isFullScreen: Boolean,
val isSendButtonVisible: Boolean = false, val mode: MessageComposerMode,
val rootThreadEventId: String? = null, val eventSink: (MessageComposerEvents) -> Unit
val startsThread: Boolean = false, ) {
// val sendMode: SendMode = SendMode.Regular("", false), val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not()
// val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle, }
// val voiceBroadcastState: VoiceBroadcastState? = null,
val text: StableCharSequence? = null,
val isFullScreen: Boolean = false,
val mode: MessageComposerMode = MessageComposerMode.Normal(""),
val eventSink: (MessageComposerEvents) -> Unit = {}
)

View file

@ -7,7 +7,7 @@ import io.element.android.x.textcomposer.TextComposer
@Composable @Composable
fun MessageComposerView( fun MessageComposerView(
state: MessageComposerState, state: MessageComposerState,
modifier: Modifier modifier: Modifier = Modifier,
) { ) {
fun onFullscreenToggle() { fun onFullscreenToggle() {

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,29 +14,31 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages package io.element.android.x.features.messages.timeline
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.diff.CacheInvalidator import io.element.android.x.features.messages.timeline.diff.CacheInvalidator
import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.x.features.messages.model.AggregatedReaction import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesItemReactionState import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEmoteContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemNoticeContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemNoticeContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.x.features.messages.util.invalidateLast import io.element.android.x.features.messages.timeline.util.invalidateLast
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.matrix.ui.MatrixItemHelper import io.element.android.x.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -50,24 +52,25 @@ import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class MessageTimelineItemStateFactory( class TimelineItemsFactory @Inject constructor(
private val matrixItemHelper: MatrixItemHelper, private val matrixItemHelper: MatrixItemHelper,
private val room: MatrixRoom, private val room: MatrixRoom,
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
) { ) {
private val timelineItemStates = MutableStateFlow<List<MessagesTimelineItemState>>(emptyList()) private val timelineItems = MutableStateFlow<List<TimelineItem>>(emptyList())
private val timelineItemStatesCache = arrayListOf<MessagesTimelineItemState?>() private val timelineItemsCache = arrayListOf<TimelineItem?>()
// Items from rust sdk, used for diffing // Items from rust sdk, used for diffing
private var timelineItems: List<MatrixTimelineItem> = emptyList() private var matrixTimelineItems: List<MatrixTimelineItem> = emptyList()
private val lock = Mutex() private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemStatesCache) private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
fun flow(): StateFlow<List<MessagesTimelineItemState>> = timelineItemStates.asStateFlow() fun flow(): StateFlow<List<TimelineItem>> = timelineItems.asStateFlow()
suspend fun replaceWith( suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
@ -83,17 +86,17 @@ class MessageTimelineItemStateFactory(
) = withContext(dispatcher) { ) = withContext(dispatcher) {
lock.withLock { lock.withLock {
// Makes sure to invalidate last as we need to recompute some data (like groupPosition) // Makes sure to invalidate last as we need to recompute some data (like groupPosition)
timelineItemStatesCache.invalidateLast() timelineItemsCache.invalidateLast()
timelineItemStatesCache.add(null) timelineItemsCache.add(null)
timelineItems = timelineItems + timelineItem matrixTimelineItems = matrixTimelineItems + timelineItem
buildAndEmitTimelineItemStates(timelineItems) buildAndEmitTimelineItemStates(matrixTimelineItems)
} }
} }
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) { private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
val newTimelineItemStates = ArrayList<MessagesTimelineItemState>() val newTimelineItemStates = ArrayList<TimelineItem>()
for (index in timelineItemStatesCache.indices.reversed()) { for (index in timelineItemsCache.indices.reversed()) {
val cacheItem = timelineItemStatesCache[index] val cacheItem = timelineItemsCache[index]
if (cacheItem == null) { if (cacheItem == null) {
buildAndCacheItem(timelineItems, index)?.also { timelineItemState -> buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
newTimelineItemStates.add(timelineItemState) newTimelineItemStates.add(timelineItemState)
@ -102,18 +105,18 @@ class MessageTimelineItemStateFactory(
newTimelineItemStates.add(cacheItem) newTimelineItemStates.add(cacheItem)
} }
} }
timelineItemStates.emit(newTimelineItemStates) this.timelineItems.emit(newTimelineItemStates)
} }
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) { private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
val timeToDiff = measureTimeMillis { val timeToDiff = measureTimeMillis {
val diffCallback = val diffCallback =
MatrixTimelineItemsDiffCallback( MatrixTimelineItemsDiffCallback(
oldList = timelineItems, oldList = matrixTimelineItems,
newList = newTimelineItems newList = newTimelineItems
) )
val diffResult = DiffUtil.calculateDiff(diffCallback, false) val diffResult = DiffUtil.calculateDiff(diffCallback, false)
timelineItems = newTimelineItems matrixTimelineItems = newTimelineItems
diffResult.dispatchUpdatesTo(cacheInvalidator) diffResult.dispatchUpdatesTo(cacheInvalidator)
} }
Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms") Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms")
@ -122,7 +125,7 @@ class MessageTimelineItemStateFactory(
private suspend fun buildAndCacheItem( private suspend fun buildAndCacheItem(
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
index: Int index: Int
): MessagesTimelineItemState? { ): TimelineItem? {
val timelineItemState = val timelineItemState =
when (val currentTimelineItem = timelineItems[index]) { when (val currentTimelineItem = timelineItems[index]) {
is MatrixTimelineItem.Event -> { is MatrixTimelineItem.Event -> {
@ -132,12 +135,12 @@ class MessageTimelineItemStateFactory(
timelineItems, timelineItems,
) )
} }
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual( is MatrixTimelineItem.Virtual -> TimelineItem.Virtual(
"virtual_item_$index" "virtual_item_$index"
) )
MatrixTimelineItem.Other -> null MatrixTimelineItem.Other -> null
} }
timelineItemStatesCache[index] = timelineItemState timelineItemsCache[index] = timelineItemState
return timelineItemState return timelineItemState
} }
@ -145,7 +148,7 @@ class MessageTimelineItemStateFactory(
currentTimelineItem: MatrixTimelineItem.Event, currentTimelineItem: MatrixTimelineItem.Event,
index: Int, index: Int,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
): MessagesTimelineItemState.MessageEvent { ): TimelineItem.MessageEvent {
val currentSender = currentTimelineItem.event.sender() val currentSender = currentTimelineItem.event.sender()
val groupPosition = val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index) computeGroupPosition(currentTimelineItem, timelineItems, index)
@ -157,8 +160,8 @@ class MessageTimelineItemStateFactory(
url = senderAvatarUrl, url = senderAvatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )
return MessagesTimelineItemState.MessageEvent( return TimelineItem.MessageEvent(
id = currentTimelineItem.uniqueId, id = EventId(currentTimelineItem.uniqueId),
senderId = currentSender, senderId = currentSender,
senderDisplayName = senderDisplayName, senderDisplayName = senderDisplayName,
senderAvatar = senderAvatarData, senderAvatar = senderAvatarData,
@ -169,24 +172,24 @@ class MessageTimelineItemStateFactory(
) )
} }
private fun MatrixTimelineItem.Event.computeReactionsState(): MessagesItemReactionState { private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val aggregatedReactions = event.reactions().map { val aggregatedReactions = event.reactions().map {
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false) AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
} }
return MessagesItemReactionState(aggregatedReactions) return TimelineItemReactions(aggregatedReactions.toImmutableList())
} }
private fun MatrixTimelineItem.Event.computeContent(): MessagesTimelineItemContent { private fun MatrixTimelineItem.Event.computeContent(): TimelineItemContent {
val content = event.content() val content = event.content()
content.asUnableToDecrypt()?.let { encryptedMessage -> content.asUnableToDecrypt()?.let { encryptedMessage ->
return MessagesTimelineItemEncryptedContent(encryptedMessage) return TimelineItemEncryptedContent(encryptedMessage)
} }
if (content.isRedactedMessage()) { if (content.isRedactedMessage()) {
return MessagesTimelineItemRedactedContent return TimelineItemRedactedContent
} }
val contentAsMessage = content.asMessage() val contentAsMessage = content.asMessage()
return when (val messageType = contentAsMessage?.msgtype()) { return when (val messageType = contentAsMessage?.msgtype()) {
is MessageType.Emote -> MessagesTimelineItemEmoteContent( is MessageType.Emote -> TimelineItemEmoteContent(
body = messageType.content.body, body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument() htmlDocument = messageType.content.formatted?.toHtmlDocument()
) )
@ -198,7 +201,7 @@ class MessageTimelineItemStateFactory(
} else { } else {
0.7f 0.7f
} }
MessagesTimelineItemImageContent( TimelineItemImageContent(
body = messageType.content.body, body = messageType.content.body,
imageMeta = MediaResolver.Meta( imageMeta = MediaResolver.Meta(
source = messageType.content.source, source = messageType.content.source,
@ -208,15 +211,15 @@ class MessageTimelineItemStateFactory(
aspectRatio = aspectRatio aspectRatio = aspectRatio
) )
} }
is MessageType.Notice -> MessagesTimelineItemNoticeContent( is MessageType.Notice -> TimelineItemNoticeContent(
body = messageType.content.body, body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument() htmlDocument = messageType.content.formatted?.toHtmlDocument()
) )
is MessageType.Text -> MessagesTimelineItemTextContent( is MessageType.Text -> TimelineItemTextContent(
body = messageType.content.body, body = messageType.content.body,
htmlDocument = messageType.content.formatted?.toHtmlDocument() htmlDocument = messageType.content.formatted?.toHtmlDocument()
) )
else -> MessagesTimelineItemUnknownContent else -> TimelineItemUnknownContent
} }
} }

View file

@ -8,15 +8,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.x.architecture.Async
import io.element.android.x.architecture.Presenter import io.element.android.x.architecture.Presenter
import io.element.android.x.features.messages.MessageTimelineItemStateFactory
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.matrix.ui.MatrixItemHelper import io.element.android.x.matrix.ui.MatrixItemHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -34,13 +33,13 @@ class TimelinePresenter @Inject constructor(
private val timeline = room.timeline() private val timeline = room.timeline()
private val matrixItemHelper = MatrixItemHelper(client) private val matrixItemHelper = MatrixItemHelper(client)
private val messageTimelineItemStateFactory = private val timelineItemsFactory =
MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default) TimelineItemsFactory(matrixItemHelper, room, Dispatchers.Default)
private class TimelineCallback(private val coroutineScope: CoroutineScope, private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory) : MatrixTimeline.Callback { private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback {
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
coroutineScope.launch { coroutineScope.launch {
messageTimelineItemStateFactory.pushItem(timelineItem) timelineItemsFactory.pushItem(timelineItem)
} }
} }
} }
@ -55,7 +54,7 @@ class TimelinePresenter @Inject constructor(
val highlightedEventId: MutableState<EventId?> = rememberSaveable { val highlightedEventId: MutableState<EventId?> = rememberSaveable {
mutableStateOf(null) mutableStateOf(null)
} }
val timelineItems = messageTimelineItemStateFactory val timelineItems = timelineItemsFactory
.flow() .flow()
.collectAsState(emptyList()) .collectAsState(emptyList())
@ -69,12 +68,12 @@ class TimelinePresenter @Inject constructor(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
timeline timeline
.timelineItems() .timelineItems()
.onEach(messageTimelineItemStateFactory::replaceWith) .onEach(timelineItemsFactory::replaceWith)
.launchIn(this) .launchIn(this)
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
timeline.callback = TimelineCallback(localCoroutineScope, messageTimelineItemStateFactory) timeline.callback = TimelineCallback(localCoroutineScope, timelineItemsFactory)
timeline.initialize() timeline.initialize()
onDispose { onDispose {
timeline.callback = null timeline.callback = null
@ -84,13 +83,13 @@ class TimelinePresenter @Inject constructor(
return TimelineState( return TimelineState(
highlightedEventId = highlightedEventId.value, highlightedEventId = highlightedEventId.value,
timelineItems = Async.Success(timelineItems.value), timelineItems = timelineItems.value.toImmutableList(),
hasMoreToLoad = hasMoreToLoad.value, hasMoreToLoad = hasMoreToLoad.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch { private fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch {
timeline.paginateBackwards(PAGINATION_COUNT) timeline.paginateBackwards(PAGINATION_COUNT)
hasMoreToLoad.value = timeline.hasMoreToLoad hasMoreToLoad.value = timeline.hasMoreToLoad
} }

View file

@ -17,14 +17,14 @@
package io.element.android.x.features.messages.timeline package io.element.android.x.features.messages.timeline
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.x.architecture.Async import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
data class TimelineState( data class TimelineState(
val timelineItems: Async<List<MessagesTimelineItemState>> = Async.Uninitialized, val timelineItems: ImmutableList<TimelineItem>,
val hasMoreToLoad: Boolean = true, val hasMoreToLoad: Boolean,
val highlightedEventId: EventId? = null, val highlightedEventId: EventId?,
val eventSink: (TimelineEvents) -> Unit = {} val eventSink: (TimelineEvents) -> Unit
) )

View file

@ -42,32 +42,31 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.element.android.x.architecture.Async
import io.element.android.x.core.compose.PairCombinedPreviewParameter import io.element.android.x.core.compose.PairCombinedPreviewParameter
import io.element.android.x.designsystem.components.avatar.Avatar import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.model.AggregatedReaction import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider import io.element.android.x.features.messages.timeline.model.TimelineItemGroupPositionProvider
import io.element.android.x.features.messages.model.MessagesItemReactionState import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider import io.element.android.x.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.x.features.messages.timeline.components.MessageEventBubble import io.element.android.x.features.messages.timeline.components.MessageEventBubble
import io.element.android.x.features.messages.timeline.components.MessagesReactionsView import io.element.android.x.features.messages.timeline.components.TimelineItemReactionsView
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemEncryptedView import io.element.android.x.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemImageView import io.element.android.x.features.messages.timeline.components.TimelineItemImageView
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemRedactedView import io.element.android.x.features.messages.timeline.components.TimelineItemRedactedView
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemTextView import io.element.android.x.features.messages.timeline.components.TimelineItemTextView
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemUnknownView import io.element.android.x.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.x.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -75,13 +74,11 @@ import kotlinx.coroutines.launch
fun TimelineView( fun TimelineView(
state: TimelineState, state: TimelineState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMessageClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {},
onMessageLongClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {}, onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {},
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val timelineItems = state.timelineItems.dataOrNull().orEmpty().toImmutableList() Box(modifier = modifier) {
Box(modifier = modifier.fillMaxWidth()) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = lazyListState, state = lazyListState,
@ -90,7 +87,7 @@ fun TimelineView(
reverseLayout = true reverseLayout = true
) { ) {
items( items(
items = timelineItems, items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() }, contentType = { timelineItem -> timelineItem.contentType() },
key = { timelineItem -> timelineItem.key() }, key = { timelineItem -> timelineItem.key() },
) { timelineItem -> ) { timelineItem ->
@ -114,36 +111,36 @@ fun TimelineView(
TimelineScrollHelper( TimelineScrollHelper(
lazyListState = lazyListState, lazyListState = lazyListState,
timelineItems = timelineItems, timelineItems = state.timelineItems,
onLoadMore = ::onReachedLoadMore onLoadMore = ::onReachedLoadMore
) )
} }
} }
private fun MessagesTimelineItemState.key(): String { private fun TimelineItem.key(): String {
return when (this) { return when (this) {
is MessagesTimelineItemState.MessageEvent -> id is TimelineItem.MessageEvent -> id.value
is MessagesTimelineItemState.Virtual -> id is TimelineItem.Virtual -> id
} }
} }
private fun MessagesTimelineItemState.contentType(): Int { private fun TimelineItem.contentType(): Int {
return when (this) { return when (this) {
is MessagesTimelineItemState.MessageEvent -> 0 is TimelineItem.MessageEvent -> 0
is MessagesTimelineItemState.Virtual -> 1 is TimelineItem.Virtual -> 1
} }
} }
@Composable @Composable
fun TimelineItemRow( fun TimelineItemRow(
timelineItem: MessagesTimelineItemState, timelineItem: TimelineItem,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit, onClick: (TimelineItem.MessageEvent) -> Unit,
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit, onLongClick: (TimelineItem.MessageEvent) -> Unit,
) { ) {
when (timelineItem) { when (timelineItem) {
is MessagesTimelineItemState.Virtual -> return is TimelineItem.Virtual -> return
is MessagesTimelineItemState.MessageEvent -> MessageEventRow( is TimelineItem.MessageEvent -> MessageEventRow(
messageEvent = timelineItem, messageEvent = timelineItem,
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
onClick = { onClick(timelineItem) }, onClick = { onClick(timelineItem) },
@ -154,7 +151,7 @@ fun TimelineItemRow(
@Composable @Composable
fun MessageEventRow( fun MessageEventRow(
messageEvent: MessagesTimelineItemState.MessageEvent, messageEvent: TimelineItem.MessageEvent,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
@ -197,32 +194,32 @@ fun MessageEventRow(
) { ) {
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
when (messageEvent.content) { when (messageEvent.content) {
is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
content = messageEvent.content, content = messageEvent.content,
modifier = contentModifier modifier = contentModifier
) )
is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView( is TimelineItemRedactedContent -> TimelineItemRedactedView(
content = messageEvent.content, content = messageEvent.content,
modifier = contentModifier modifier = contentModifier
) )
is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView( is TimelineItemTextBasedContent -> TimelineItemTextView(
content = messageEvent.content, content = messageEvent.content,
interactionSource = interactionSource, interactionSource = interactionSource,
modifier = contentModifier, modifier = contentModifier,
onTextClicked = onClick, onTextClicked = onClick,
onTextLongClicked = onLongClick onTextLongClicked = onLongClick
) )
is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView( is TimelineItemUnknownContent -> TimelineItemUnknownView(
content = messageEvent.content, content = messageEvent.content,
modifier = contentModifier modifier = contentModifier
) )
is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView( is TimelineItemImageContent -> TimelineItemImageView(
content = messageEvent.content, content = messageEvent.content,
modifier = contentModifier modifier = contentModifier
) )
} }
} }
MessagesReactionsView( TimelineItemReactionsView(
reactionsState = messageEvent.reactionsState, reactionsState = messageEvent.reactionsState,
modifier = Modifier modifier = Modifier
.zIndex(1f) .zIndex(1f)
@ -264,7 +261,7 @@ private fun MessageSenderInformation(
@Composable @Composable
internal fun BoxScope.TimelineScrollHelper( internal fun BoxScope.TimelineScrollHelper(
lazyListState: LazyListState, lazyListState: LazyListState,
timelineItems: ImmutableList<MessagesTimelineItemState>, timelineItems: ImmutableList<TimelineItem>,
onLoadMore: () -> Unit = {}, onLoadMore: () -> Unit = {},
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -338,8 +335,8 @@ internal fun TimelineLoadingMoreIndicator() {
} }
class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
PairCombinedPreviewParameter<MessagesItemGroupPosition, MessagesTimelineItemContent>( PairCombinedPreviewParameter<MessagesItemGroupPosition, TimelineItemContent>(
MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider() TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider()
) )
@Suppress("PreviewPublic") @Suppress("PreviewPublic")
@ -347,7 +344,7 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
@Composable @Composable
fun TimelineItemsPreview( fun TimelineItemsPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class) @PreviewParameter(MessagesTimelineItemContentProvider::class)
content: MessagesTimelineItemContent content: TimelineItemContent
) { ) {
val timelineItems = persistentListOf( val timelineItems = persistentListOf(
// 3 items (First Middle Last) with isMine = false // 3 items (First Middle Last) with isMine = false
@ -385,23 +382,26 @@ fun TimelineItemsPreview(
) )
TimelineView( TimelineView(
state = TimelineState( state = TimelineState(
timelineItems = Async.Success(timelineItems) timelineItems = timelineItems,
hasMoreToLoad = true,
highlightedEventId = null,
eventSink = {}
) )
) )
} }
private fun createMessageEvent( private fun createMessageEvent(
isMine: Boolean, isMine: Boolean,
content: MessagesTimelineItemContent, content: TimelineItemContent,
groupPosition: MessagesItemGroupPosition groupPosition: MessagesItemGroupPosition
): MessagesTimelineItemState { ): TimelineItem {
return MessagesTimelineItemState.MessageEvent( return TimelineItem.MessageEvent(
id = Math.random().toString(), id = EventId(Math.random().toString()),
senderId = "senderId", senderId = "senderId",
senderAvatar = AvatarData("sender"), senderAvatar = AvatarData("sender"),
content = content, content = content,
reactionsState = MessagesItemReactionState( reactionsState = TimelineItemReactions(
listOf( persistentListOf(
AggregatedReaction("👍", "1") AggregatedReaction("👍", "1")
) )
), ),

View file

@ -36,7 +36,7 @@ import io.element.android.x.designsystem.SystemGrey5Dark
import io.element.android.x.designsystem.SystemGrey5Light import io.element.android.x.designsystem.SystemGrey5Light
import io.element.android.x.designsystem.SystemGrey6Dark import io.element.android.x.designsystem.SystemGrey6Dark
import io.element.android.x.designsystem.SystemGrey6Light import io.element.android.x.designsystem.SystemGrey6Light
import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
private val BUBBLE_RADIUS = 16.dp private val BUBBLE_RADIUS = 16.dp

View file

@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
@Composable @Composable
fun MessagesTimelineItemEncryptedView( fun TimelineItemEncryptedView(
content: MessagesTimelineItemEncryptedContent, content: TimelineItemEncryptedContent,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
MessagesTimelineItemInformativeView( TimelineItemInformativeView(
text = "Decryption error", text = "Decryption error",
iconDescription = "Warning", iconDescription = "Warning",
icon = Icons.Default.Warning, icon = Icons.Default.Warning,

View file

@ -33,11 +33,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
@Composable @Composable
fun MessagesTimelineItemImageView( fun TimelineItemImageView(
content: MessagesTimelineItemImageContent, content: TimelineItemImageContent,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val widthPercent = if (content.aspectRatio > 1f) { val widthPercent = if (content.aspectRatio > 1f) {

View file

@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@Composable @Composable
fun MessagesTimelineItemInformativeView( fun TimelineItemInformativeView(
text: String, text: String,
iconDescription: String, iconDescription: String,
icon: ImageVector, icon: ImageVector,

View file

@ -32,12 +32,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.features.messages.model.AggregatedReaction import io.element.android.x.features.messages.timeline.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemReactionState import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
@Composable @Composable
fun MessagesReactionsView( fun TimelineItemReactionsView(
reactionsState: MessagesItemReactionState, reactionsState: TimelineItemReactions,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (reactionsState.reactions.isEmpty()) return if (reactionsState.reactions.isEmpty()) return

View file

@ -20,15 +20,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemInformativeView
@Composable @Composable
fun MessagesTimelineItemRedactedView( fun TimelineItemRedactedView(
content: MessagesTimelineItemRedactedContent, content: TimelineItemRedactedContent,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
MessagesTimelineItemInformativeView( TimelineItemInformativeView(
text = "This message has been deleted", text = "This message has been deleted",
iconDescription = "Delete", iconDescription = "Delete",
icon = Icons.Default.Delete, icon = Icons.Default.Delete,

View file

@ -31,11 +31,11 @@ import androidx.core.text.util.LinkifyCompat
import io.element.android.x.designsystem.LinkColor import io.element.android.x.designsystem.LinkColor
import io.element.android.x.designsystem.components.ClickableLinkText import io.element.android.x.designsystem.components.ClickableLinkText
import io.element.android.x.features.messages.timeline.components.html.HtmlDocument import io.element.android.x.features.messages.timeline.components.html.HtmlDocument
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
@Composable @Composable
fun MessagesTimelineItemTextView( fun TimelineItemTextView(
content: MessagesTimelineItemTextBasedContent, content: TimelineItemTextBasedContent,
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {}, onTextClicked: () -> Unit = {},

View file

@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
@Composable @Composable
fun MessagesTimelineItemUnknownView( fun TimelineItemUnknownView(
content: MessagesTimelineItemUnknownContent, content: TimelineItemUnknownContent,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
MessagesTimelineItemInformativeView( TimelineItemInformativeView(
text = "Event not handled by EAX", text = "Event not handled by EAX",
iconDescription = "Info", iconDescription = "Info",
icon = Icons.Default.Info, icon = Icons.Default.Info,

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,14 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.diff package io.element.android.x.features.messages.timeline.diff
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.timeline.model.TimelineItem
import io.element.android.x.features.messages.util.invalidateLast import io.element.android.x.features.messages.timeline.util.invalidateLast
import timber.log.Timber import timber.log.Timber
internal class CacheInvalidator(private val itemStatesCache: MutableList<MessagesTimelineItemState?>) : internal class CacheInvalidator(private val itemStatesCache: MutableList<TimelineItem?>) :
ListUpdateCallback { ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.diff package io.element.android.x.features.messages.timeline.diff
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.timeline.MatrixTimelineItem

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,27 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model package io.element.android.x.features.messages.timeline.model
import androidx.compose.runtime.Immutable
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.x.matrix.core.EventId
sealed interface MessagesTimelineItemState { @Immutable
sealed interface TimelineItem {
data class Virtual( data class Virtual(
val id: String val id: String
) : MessagesTimelineItemState ) : TimelineItem
data class MessageEvent( data class MessageEvent(
val id: String, val id: EventId,
val senderId: String, val senderId: String,
val senderDisplayName: String?, val senderDisplayName: String?,
val senderAvatar: AvatarData, val senderAvatar: AvatarData,
val content: MessagesTimelineItemContent, val content: TimelineItemContent,
val sentTime: String = "", val sentTime: String = "",
val isMine: Boolean = false, val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None, val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
val reactionsState: MessagesItemReactionState val reactionsState: TimelineItemReactions
) : MessagesTimelineItemState { ) : TimelineItem {
val showSenderInformation = groupPosition.isNew() && !isMine val showSenderInformation = groupPosition.isNew() && !isMine

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,10 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model package io.element.android.x.features.messages.timeline.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@Immutable
sealed interface MessagesItemGroupPosition { sealed interface MessagesItemGroupPosition {
object First : MessagesItemGroupPosition object First : MessagesItemGroupPosition
object Middle : MessagesItemGroupPosition object Middle : MessagesItemGroupPosition
@ -30,7 +32,7 @@ sealed interface MessagesItemGroupPosition {
} }
} }
internal class MessagesItemGroupPositionProvider : PreviewParameterProvider<MessagesItemGroupPosition> { internal class TimelineItemGroupPositionProvider : PreviewParameterProvider<MessagesItemGroupPosition> {
override val values = sequenceOf( override val values = sequenceOf(
MessagesItemGroupPosition.First, MessagesItemGroupPosition.First,
MessagesItemGroupPosition.Middle, MessagesItemGroupPosition.Middle,

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,16 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model package io.element.android.x.features.messages.timeline.model
import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList
@Stable data class TimelineItemReactions(
data class MessagesItemReactionState( val reactions: ImmutableList<AggregatedReaction>
val reactions: List<AggregatedReaction>
) )
@Stable
data class AggregatedReaction( data class AggregatedReaction(
val key: String, val key: String,
val count: String, val count: String,

View file

@ -14,32 +14,32 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage import org.matrix.rustcomponents.sdk.EncryptedMessage
sealed interface MessagesTimelineItemContent sealed interface TimelineItemContent
class MessagesTimelineItemContentProvider : PreviewParameterProvider<MessagesTimelineItemContent> { class MessagesTimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
override val values = sequenceOf( override val values = sequenceOf(
MessagesTimelineItemEmoteContent( TimelineItemEmoteContent(
body = "Emote", body = "Emote",
htmlDocument = null htmlDocument = null
), ),
MessagesTimelineItemEncryptedContent( TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown encryptedMessage = EncryptedMessage.Unknown
), ),
// TODO MessagesTimelineItemImageContent(), // TODO MessagesTimelineItemImageContent(),
MessagesTimelineItemNoticeContent( TimelineItemNoticeContent(
body = "Notice", body = "Notice",
htmlDocument = null htmlDocument = null
), ),
MessagesTimelineItemRedactedContent, TimelineItemRedactedContent,
MessagesTimelineItemTextContent( TimelineItemTextContent(
body = "Text", body = "Text",
htmlDocument = null htmlDocument = null
), ),
MessagesTimelineItemUnknownContent, TimelineItemUnknownContent,
) )
} }

View file

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
data class MessagesTimelineItemTextContent( data class TimelineItemEmoteContent(
override val body: String, override val body: String,
override val htmlDocument: Document? override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent ) : TimelineItemTextBasedContent

View file

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import org.matrix.rustcomponents.sdk.EncryptedMessage import org.matrix.rustcomponents.sdk.EncryptedMessage
data class MessagesTimelineItemEncryptedContent( data class TimelineItemEncryptedContent(
val encryptedMessage: EncryptedMessage val encryptedMessage: EncryptedMessage
) : MessagesTimelineItemContent ) : TimelineItemContent

View file

@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.media.MediaResolver
data class MessagesTimelineItemImageContent( data class TimelineItemImageContent(
val body: String, val body: String,
val imageMeta: MediaResolver.Meta, val imageMeta: MediaResolver.Meta,
val blurhash: String?, val blurhash: String?,
val aspectRatio: Float val aspectRatio: Float
) : MessagesTimelineItemContent ) : TimelineItemContent

View file

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
data class MessagesTimelineItemEmoteContent( data class TimelineItemNoticeContent(
override val body: String, override val body: String,
override val htmlDocument: Document? override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent ) : TimelineItemTextBasedContent

View file

@ -14,6 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
object MessagesTimelineItemUnknownContent : MessagesTimelineItemContent object TimelineItemRedactedContent : TimelineItemContent

View file

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
sealed interface MessagesTimelineItemTextBasedContent : MessagesTimelineItemContent { sealed interface TimelineItemTextBasedContent : TimelineItemContent {
val body: String val body: String
val htmlDocument: Document? val htmlDocument: Document?
} }

View file

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
data class MessagesTimelineItemNoticeContent( data class TimelineItemTextContent(
override val body: String, override val body: String,
override val htmlDocument: Document? override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent ) : TimelineItemTextBasedContent

View file

@ -14,6 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.model.content package io.element.android.x.features.messages.timeline.model.content
object MessagesTimelineItemRedactedContent : MessagesTimelineItemContent object TimelineItemUnknownContent : TimelineItemContent

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.features.messages.util package io.element.android.x.features.messages.timeline.util
internal inline fun <reified T> MutableList<T?>.invalidateLast() { internal inline fun <reified T> MutableList<T?>.invalidateLast() {
val indexOfLast = size val indexOfLast = size

View file

@ -25,30 +25,23 @@ import io.element.android.x.matrix.core.SessionId
import io.element.android.x.matrix.session.SessionStore import io.element.android.x.matrix.session.SessionStore
import io.element.android.x.matrix.session.sessionId import io.element.android.x.matrix.session.sessionId
import io.element.android.x.matrix.util.logError import io.element.android.x.matrix.util.logError
import java.io.File
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder
import timber.log.Timber import timber.log.Timber
import java.io.File
import javax.inject.Inject
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class Matrix @Inject constructor( class Matrix @Inject constructor(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
@ApplicationContext context: Context, @ApplicationContext context: Context,
) { ) {
private val coroutineDispatchers = CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
private val baseDirectory = File(context.filesDir, "sessions") private val baseDirectory = File(context.filesDir, "sessions")
private val sessionStore = SessionStore(context) private val sessionStore = SessionStore(context)
private val authService = AuthenticationService(baseDirectory.absolutePath) private val authService = AuthenticationService(baseDirectory.absolutePath)
@ -57,7 +50,7 @@ class Matrix @Inject constructor(
return sessionStore.isLoggedIn() return sessionStore.isLoggedIn()
} }
suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io){ suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession()?.sessionId() sessionStore.getLatestSession()?.sessionId()
} }

View file

@ -17,6 +17,7 @@
package io.element.android.x.matrix.room package io.element.android.x.matrix.room
import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimeline
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -39,13 +40,15 @@ class MatrixRoom(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
fun syncUpdateFlow(): Flow<Unit> { fun syncUpdateFlow(): Flow<Long> {
return slidingSyncUpdateFlow return slidingSyncUpdateFlow
.filter { .filter {
it.rooms.contains(room.id()) it.rooms.contains(room.id())
} }
.map { } .map {
.onStart { emit(Unit) } System.currentTimeMillis()
}
.onStart { emit(System.currentTimeMillis()) }
} }
fun timeline(): MatrixTimeline { fun timeline(): MatrixTimeline {
@ -107,26 +110,26 @@ class MatrixRoom(
} }
} }
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) { suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
// val content = messageEventContentFromMarkdown(message) // val content = messageEventContentFromMarkdown(message)
runCatching { runCatching {
room.edit(/* TODO use content */ message, originalEventId, transactionId) room.edit(/* TODO use content */ message, originalEventId.value, transactionId)
} }
} }
suspend fun replyMessage(eventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) { suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
// val content = messageEventContentFromMarkdown(message) // val content = messageEventContentFromMarkdown(message)
runCatching { runCatching {
room.sendReply(/* TODO use content */ message, eventId, transactionId) room.sendReply(/* TODO use content */ message, eventId.value, transactionId)
} }
} }
suspend fun redactEvent(eventId: String, reason: String? = null) = withContext(coroutineDispatchers.io) { suspend fun redactEvent(eventId: EventId, reason: String? = null) = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
runCatching { runCatching {
room.redact(eventId, reason, transactionId) room.redact(eventId.value, reason, transactionId)
} }
} }
} }

View file

@ -17,6 +17,7 @@
package io.element.android.x.matrix.timeline package io.element.android.x.matrix.timeline
import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -146,11 +147,11 @@ class MatrixTimeline(
return matrixRoom.sendMessage(message) return matrixRoom.sendMessage(message)
} }
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> { suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
return matrixRoom.editMessage(originalEventId, message = message) return matrixRoom.editMessage(originalEventId, message = message)
} }
suspend fun replyMessage(inReplyToEventId: String, message: String): Result<Unit> { suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return matrixRoom.replyMessage(inReplyToEventId, message) return matrixRoom.replyMessage(inReplyToEventId, message)
} }

View file

@ -26,8 +26,9 @@ import io.element.android.x.matrix.ui.model.MatrixUser
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import javax.inject.Inject
class MatrixItemHelper( class MatrixItemHelper @Inject constructor(
private val client: MatrixClient private val client: MatrixClient
) { ) {
/** /**

View file

@ -32,6 +32,7 @@ android {
dependencies { dependencies {
implementation(project(":libraries:elementresources")) implementation(project(":libraries:elementresources"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(libs.wysiwyg) implementation(libs.wysiwyg)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation("com.google.android.material:material:1.7.0") implementation("com.google.android.material:material:1.7.0")

View file

@ -17,31 +17,32 @@
package io.element.android.x.textcomposer package io.element.android.x.textcomposer
import android.os.Parcelable import android.os.Parcelable
import io.element.android.x.matrix.core.EventId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
sealed interface MessageComposerMode : Parcelable { sealed interface MessageComposerMode : Parcelable {
@Parcelize @Parcelize
data class Normal(val content: CharSequence?) : MessageComposerMode data class Normal(val content: CharSequence?) : MessageComposerMode
sealed class Special(open val eventId: String, open val defaultContent: CharSequence) : sealed class Special(open val eventId: EventId, open val defaultContent: CharSequence) :
MessageComposerMode MessageComposerMode
@Parcelize @Parcelize
data class Edit(override val eventId: String, override val defaultContent: CharSequence) : data class Edit(override val eventId: EventId, override val defaultContent: CharSequence) :
Special(eventId, defaultContent) Special(eventId, defaultContent)
@Parcelize @Parcelize
class Quote(override val eventId: String, override val defaultContent: CharSequence) : class Quote(override val eventId: EventId, override val defaultContent: CharSequence) :
Special(eventId, defaultContent) Special(eventId, defaultContent)
@Parcelize @Parcelize
class Reply( class Reply(
val senderName: String, val senderName: String,
override val eventId: String, override val eventId: EventId,
override val defaultContent: CharSequence override val defaultContent: CharSequence
) : Special(eventId, defaultContent) ) : Special(eventId, defaultContent)
val relatedEventId: String? val relatedEventId: EventId?
get() = when (this) { get() = when (this) {
is Normal -> null is Normal -> null
is Edit -> eventId is Edit -> eventId