Finish migration of Messages screen
This commit is contained in:
@@ -19,10 +19,14 @@ package io.element.android.x.di
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.x.core.coroutine.CoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.plus
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
@@ -33,4 +37,15 @@ object AppModule {
|
||||
fun providesAppCoroutineScope(): CoroutineScope {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.element.android.x.features.messages
|
||||
|
||||
import io.element.android.x.features.messages.actionlist.TimelineItemAction
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class MessagesNode @AssistedInject constructor(
|
||||
MessagesView(
|
||||
state = state,
|
||||
onBackPressed = this::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.AvatarSize
|
||||
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.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||
import io.element.android.x.features.messages.actionlist.model.TimelineItemAction
|
||||
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.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.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.room.MatrixRoom
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
@@ -60,7 +61,9 @@ class MessagesPresenter @Inject constructor(
|
||||
)
|
||||
roomName.value = room.name
|
||||
}
|
||||
|
||||
LaunchedEffect(composerState.mode.relatedEventId) {
|
||||
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
||||
}
|
||||
fun handleEvents(event: MessagesEvents) {
|
||||
when (event) {
|
||||
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.messageEvent, composerState)
|
||||
@@ -79,7 +82,7 @@ class MessagesPresenter @Inject constructor(
|
||||
|
||||
fun CoroutineScope.handleTimelineAction(
|
||||
action: TimelineItemAction,
|
||||
targetEvent: MessagesTimelineItemState.MessageEvent,
|
||||
targetEvent: TimelineItem.MessageEvent,
|
||||
composerState: MessageComposerState,
|
||||
) = launch {
|
||||
when (action) {
|
||||
@@ -95,21 +98,21 @@ class MessagesPresenter @Inject constructor(
|
||||
Timber.v("NotImplementedYet")
|
||||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) {
|
||||
private suspend fun handleActionRedact(event: TimelineItem.MessageEvent) {
|
||||
room.redactEvent(event.id)
|
||||
}
|
||||
|
||||
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent, composerState: MessageComposerState) {
|
||||
private fun handleActionEdit(targetEvent: TimelineItem.MessageEvent, composerState: MessageComposerState) {
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.id,
|
||||
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty()
|
||||
)
|
||||
composerState.eventSink(
|
||||
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, "")
|
||||
composerState.eventSink(
|
||||
MessageComposerEvents.SetMode(composerMode)
|
||||
|
||||
@@ -10,10 +10,10 @@ import io.element.android.x.matrix.core.RoomId
|
||||
@Immutable
|
||||
data class MessagesState(
|
||||
val roomId: RoomId,
|
||||
val roomName: String? = null,
|
||||
val roomAvatar: AvatarData? = null,
|
||||
val roomName: String?,
|
||||
val roomAvatar: AvatarData?,
|
||||
val composerState: MessageComposerState,
|
||||
val timelineState: TimelineState,
|
||||
val actionListState: ActionListState,
|
||||
val eventSink: (MessagesEvents) -> Unit = {}
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
||||
@@ -48,8 +48,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.designsystem.components.avatar.Avatar
|
||||
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.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.timeline.TimelineView
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
@@ -76,8 +80,28 @@ fun MessagesView(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
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(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
@@ -92,6 +116,8 @@ fun MessagesView(
|
||||
MessagesViewContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
@@ -102,10 +128,6 @@ fun MessagesView(
|
||||
},
|
||||
)
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, messageEvent: MessagesTimelineItemState.MessageEvent) {
|
||||
state.eventSink(MessagesEvents.HandleAction(action, messageEvent))
|
||||
}
|
||||
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
modalBottomSheetState = itemActionsBottomSheetState,
|
||||
@@ -116,43 +138,32 @@ fun MessagesView(
|
||||
@Composable
|
||||
fun MessagesViewContent(
|
||||
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(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
// Hide timeline if composer is full screen
|
||||
if (!state.composerState.isFullScreen) {
|
||||
TimelineView(
|
||||
state = state.timelineState,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked
|
||||
modifier = Modifier.weight(1f),
|
||||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked
|
||||
)
|
||||
}
|
||||
MessageComposerView(
|
||||
state = state.composerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let {
|
||||
if (state.composerState.isFullScreen) {
|
||||
it.weight(1f, fill = false)
|
||||
} else {
|
||||
it.wrapContentHeight(Alignment.Bottom)
|
||||
}
|
||||
},
|
||||
.wrapContentHeight(Alignment.Bottom)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 {
|
||||
object Clear : ActionListEvents
|
||||
data class ComputeForMessage(val messageEvent: MessagesTimelineItemState.MessageEvent) : ActionListEvents
|
||||
data class ComputeForMessage(val messageEvent: TimelineItem.MessageEvent) : ActionListEvents
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.x.architecture.Presenter
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||
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.timeline.model.content.TimelineItemRedactedContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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 {
|
||||
target.value = ActionListState.Target.Loading(messagesTimelineItemState)
|
||||
fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.MessageEvent, target: MutableState<ActionListState.Target>) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val actions =
|
||||
if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) {
|
||||
if (timelineItem.content is TimelineItemRedactedContent) {
|
||||
emptyList()
|
||||
} else {
|
||||
mutableListOf(
|
||||
@@ -48,12 +49,12 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
).also {
|
||||
if (messagesTimelineItemState.isMine) {
|
||||
if (timelineItem.isMine) {
|
||||
it.add(TimelineItemAction.Edit)
|
||||
it.add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
target.value = ActionListState.Target.Success(messagesTimelineItemState, actions.toImmutableList())
|
||||
target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,21 @@
|
||||
package io.element.android.x.features.messages.actionlist
|
||||
|
||||
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
|
||||
|
||||
@Immutable
|
||||
data class ActionListState(
|
||||
val target: Target = Target.None,
|
||||
val eventSink: (ActionListEvents) -> Unit = {},
|
||||
val target: Target,
|
||||
val eventSink: (ActionListEvents) -> Unit,
|
||||
) {
|
||||
|
||||
sealed interface Target {
|
||||
object None : Target
|
||||
data class Loading(val messageEvent: MessagesTimelineItemState.MessageEvent) : Target
|
||||
data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target
|
||||
data class Success(
|
||||
val messageEvent: MessagesTimelineItemState.MessageEvent,
|
||||
val messageEvent: TimelineItem.MessageEvent,
|
||||
val actions: ImmutableList<TimelineItemAction>,
|
||||
) : Target
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.launch
|
||||
|
||||
@@ -34,7 +35,7 @@ import kotlinx.coroutines.launch
|
||||
fun ActionListView(
|
||||
state: ActionListState,
|
||||
modalBottomSheetState: ModalBottomSheetState,
|
||||
onActionSelected: (action: TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onActionSelected: (action: TimelineItemAction, TimelineItem.MessageEvent) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -48,7 +49,7 @@ fun ActionListView(
|
||||
|
||||
fun onItemActionClicked(
|
||||
itemAction: TimelineItemAction,
|
||||
targetItem: MessagesTimelineItemState.MessageEvent
|
||||
targetItem: TimelineItem.MessageEvent
|
||||
) {
|
||||
onActionSelected(itemAction, targetItem)
|
||||
coroutineScope.launch {
|
||||
@@ -75,7 +76,7 @@ fun ActionListView(
|
||||
private fun SheetContent(
|
||||
state: ActionListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onActionClicked: (TimelineItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> },
|
||||
onActionClicked: (TimelineItemAction, TimelineItem.MessageEvent) -> Unit = { _, _ -> },
|
||||
) {
|
||||
when (val target = state.target) {
|
||||
is ActionListState.Target.Loading,
|
||||
|
||||
@@ -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.x.features.messages.actionlist
|
||||
package io.element.android.x.features.messages.actionlist.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
@@ -1,12 +1,13 @@
|
||||
package io.element.android.x.features.messages.textcomposer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.textcomposer.MessageComposerMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -15,7 +16,6 @@ import javax.inject.Inject
|
||||
|
||||
class MessageComposerPresenter @Inject constructor(
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val client: MatrixClient,
|
||||
private val room: MatrixRoom
|
||||
) : Presenter<MessageComposerState> {
|
||||
|
||||
@@ -24,25 +24,32 @@ class MessageComposerPresenter @Inject constructor(
|
||||
val isFullScreen = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val text: MutableState<CharSequence> = rememberSaveable {
|
||||
mutableStateOf("")
|
||||
val text: MutableState<StableCharSequence> = rememberSaveable {
|
||||
mutableStateOf(StableCharSequence(""))
|
||||
}
|
||||
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
|
||||
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) {
|
||||
when (event) {
|
||||
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()
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode)
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
|
||||
is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode
|
||||
}
|
||||
}
|
||||
|
||||
return MessageComposerState(
|
||||
text = text.value.toStableCharSequence(),
|
||||
text = text.value,
|
||||
isFullScreen = isFullScreen.value,
|
||||
mode = composerMode.value,
|
||||
eventSink = ::handleEvents
|
||||
@@ -53,9 +60,10 @@ class MessageComposerPresenter @Inject constructor(
|
||||
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
|
||||
// Reset composer right away
|
||||
textState.value = "".toStableCharSequence()
|
||||
composerMode.setToNormal()
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(text)
|
||||
|
||||
@@ -22,16 +22,10 @@ import io.element.android.x.textcomposer.MessageComposerMode
|
||||
|
||||
@Immutable
|
||||
data class MessageComposerState(
|
||||
// val roomId: String,
|
||||
// val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
|
||||
val isSendButtonVisible: Boolean = false,
|
||||
val rootThreadEventId: String? = null,
|
||||
val startsThread: Boolean = false,
|
||||
// val sendMode: SendMode = SendMode.Regular("", false),
|
||||
// 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 = {}
|
||||
)
|
||||
val text: StableCharSequence?,
|
||||
val isFullScreen: Boolean,
|
||||
val mode: MessageComposerMode,
|
||||
val eventSink: (MessageComposerEvents) -> Unit
|
||||
) {
|
||||
val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.element.android.x.textcomposer.TextComposer
|
||||
@Composable
|
||||
fun MessageComposerView(
|
||||
state: MessageComposerState,
|
||||
modifier: Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
fun onFullscreenToggle() {
|
||||
|
||||
@@ -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,29 +14,31 @@
|
||||
* 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 io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.x.features.messages.diff.CacheInvalidator
|
||||
import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback
|
||||
import io.element.android.x.features.messages.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.model.MessagesItemReactionState
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEmoteContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemNoticeContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
|
||||
import io.element.android.x.features.messages.util.invalidateLast
|
||||
import io.element.android.x.features.messages.timeline.diff.CacheInvalidator
|
||||
import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemNoticeContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemUnknownContent
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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.MessageType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class MessageTimelineItemStateFactory(
|
||||
class TimelineItemsFactory @Inject constructor(
|
||||
private val matrixItemHelper: MatrixItemHelper,
|
||||
private val room: MatrixRoom,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
private val timelineItemStates = MutableStateFlow<List<MessagesTimelineItemState>>(emptyList())
|
||||
private val timelineItemStatesCache = arrayListOf<MessagesTimelineItemState?>()
|
||||
private val timelineItems = MutableStateFlow<List<TimelineItem>>(emptyList())
|
||||
private val timelineItemsCache = arrayListOf<TimelineItem?>()
|
||||
|
||||
// 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 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(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
@@ -83,17 +86,17 @@ class MessageTimelineItemStateFactory(
|
||||
) = withContext(dispatcher) {
|
||||
lock.withLock {
|
||||
// Makes sure to invalidate last as we need to recompute some data (like groupPosition)
|
||||
timelineItemStatesCache.invalidateLast()
|
||||
timelineItemStatesCache.add(null)
|
||||
timelineItems = timelineItems + timelineItem
|
||||
buildAndEmitTimelineItemStates(timelineItems)
|
||||
timelineItemsCache.invalidateLast()
|
||||
timelineItemsCache.add(null)
|
||||
matrixTimelineItems = matrixTimelineItems + timelineItem
|
||||
buildAndEmitTimelineItemStates(matrixTimelineItems)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
|
||||
val newTimelineItemStates = ArrayList<MessagesTimelineItemState>()
|
||||
for (index in timelineItemStatesCache.indices.reversed()) {
|
||||
val cacheItem = timelineItemStatesCache[index]
|
||||
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)
|
||||
@@ -102,18 +105,18 @@ class MessageTimelineItemStateFactory(
|
||||
newTimelineItemStates.add(cacheItem)
|
||||
}
|
||||
}
|
||||
timelineItemStates.emit(newTimelineItemStates)
|
||||
this.timelineItems.emit(newTimelineItemStates)
|
||||
}
|
||||
|
||||
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
|
||||
val timeToDiff = measureTimeMillis {
|
||||
val diffCallback =
|
||||
MatrixTimelineItemsDiffCallback(
|
||||
oldList = timelineItems,
|
||||
oldList = matrixTimelineItems,
|
||||
newList = newTimelineItems
|
||||
)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback, false)
|
||||
timelineItems = newTimelineItems
|
||||
matrixTimelineItems = newTimelineItems
|
||||
diffResult.dispatchUpdatesTo(cacheInvalidator)
|
||||
}
|
||||
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(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
index: Int
|
||||
): MessagesTimelineItemState? {
|
||||
): TimelineItem? {
|
||||
val timelineItemState =
|
||||
when (val currentTimelineItem = timelineItems[index]) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
@@ -132,12 +135,12 @@ class MessageTimelineItemStateFactory(
|
||||
timelineItems,
|
||||
)
|
||||
}
|
||||
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
||||
is MatrixTimelineItem.Virtual -> TimelineItem.Virtual(
|
||||
"virtual_item_$index"
|
||||
)
|
||||
MatrixTimelineItem.Other -> null
|
||||
}
|
||||
timelineItemStatesCache[index] = timelineItemState
|
||||
timelineItemsCache[index] = timelineItemState
|
||||
return timelineItemState
|
||||
}
|
||||
|
||||
@@ -145,7 +148,7 @@ class MessageTimelineItemStateFactory(
|
||||
currentTimelineItem: MatrixTimelineItem.Event,
|
||||
index: Int,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
): MessagesTimelineItemState.MessageEvent {
|
||||
): TimelineItem.MessageEvent {
|
||||
val currentSender = currentTimelineItem.event.sender()
|
||||
val groupPosition =
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
@@ -157,8 +160,8 @@ class MessageTimelineItemStateFactory(
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
return MessagesTimelineItemState.MessageEvent(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
return TimelineItem.MessageEvent(
|
||||
id = EventId(currentTimelineItem.uniqueId),
|
||||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
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 {
|
||||
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()
|
||||
content.asUnableToDecrypt()?.let { encryptedMessage ->
|
||||
return MessagesTimelineItemEncryptedContent(encryptedMessage)
|
||||
return TimelineItemEncryptedContent(encryptedMessage)
|
||||
}
|
||||
if (content.isRedactedMessage()) {
|
||||
return MessagesTimelineItemRedactedContent
|
||||
return TimelineItemRedactedContent
|
||||
}
|
||||
val contentAsMessage = content.asMessage()
|
||||
return when (val messageType = contentAsMessage?.msgtype()) {
|
||||
is MessageType.Emote -> MessagesTimelineItemEmoteContent(
|
||||
is MessageType.Emote -> TimelineItemEmoteContent(
|
||||
body = messageType.content.body,
|
||||
htmlDocument = messageType.content.formatted?.toHtmlDocument()
|
||||
)
|
||||
@@ -198,7 +201,7 @@ class MessageTimelineItemStateFactory(
|
||||
} else {
|
||||
0.7f
|
||||
}
|
||||
MessagesTimelineItemImageContent(
|
||||
TimelineItemImageContent(
|
||||
body = messageType.content.body,
|
||||
imageMeta = MediaResolver.Meta(
|
||||
source = messageType.content.source,
|
||||
@@ -208,15 +211,15 @@ class MessageTimelineItemStateFactory(
|
||||
aspectRatio = aspectRatio
|
||||
)
|
||||
}
|
||||
is MessageType.Notice -> MessagesTimelineItemNoticeContent(
|
||||
is MessageType.Notice -> TimelineItemNoticeContent(
|
||||
body = messageType.content.body,
|
||||
htmlDocument = messageType.content.formatted?.toHtmlDocument()
|
||||
)
|
||||
is MessageType.Text -> MessagesTimelineItemTextContent(
|
||||
is MessageType.Text -> TimelineItemTextContent(
|
||||
body = messageType.content.body,
|
||||
htmlDocument = messageType.content.formatted?.toHtmlDocument()
|
||||
)
|
||||
else -> MessagesTimelineItemUnknownContent
|
||||
else -> TimelineItemUnknownContent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@ 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.x.architecture.Async
|
||||
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.core.EventId
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import io.element.android.x.matrix.ui.MatrixItemHelper
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -34,13 +33,13 @@ class TimelinePresenter @Inject constructor(
|
||||
|
||||
private val timeline = room.timeline()
|
||||
private val matrixItemHelper = MatrixItemHelper(client)
|
||||
private val messageTimelineItemStateFactory =
|
||||
MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default)
|
||||
private val timelineItemsFactory =
|
||||
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) {
|
||||
coroutineScope.launch {
|
||||
messageTimelineItemStateFactory.pushItem(timelineItem)
|
||||
timelineItemsFactory.pushItem(timelineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +54,7 @@ class TimelinePresenter @Inject constructor(
|
||||
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val timelineItems = messageTimelineItemStateFactory
|
||||
val timelineItems = timelineItemsFactory
|
||||
.flow()
|
||||
.collectAsState(emptyList())
|
||||
|
||||
@@ -69,12 +68,12 @@ class TimelinePresenter @Inject constructor(
|
||||
LaunchedEffect(Unit) {
|
||||
timeline
|
||||
.timelineItems()
|
||||
.onEach(messageTimelineItemStateFactory::replaceWith)
|
||||
.onEach(timelineItemsFactory::replaceWith)
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
timeline.callback = TimelineCallback(localCoroutineScope, messageTimelineItemStateFactory)
|
||||
timeline.callback = TimelineCallback(localCoroutineScope, timelineItemsFactory)
|
||||
timeline.initialize()
|
||||
onDispose {
|
||||
timeline.callback = null
|
||||
@@ -84,13 +83,13 @@ class TimelinePresenter @Inject constructor(
|
||||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
timelineItems = Async.Success(timelineItems.value),
|
||||
timelineItems = timelineItems.value.toImmutableList(),
|
||||
hasMoreToLoad = hasMoreToLoad.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch {
|
||||
private fun CoroutineScope.loadMore(hasMoreToLoad: MutableState<Boolean>) = launch {
|
||||
timeline.paginateBackwards(PAGINATION_COUNT)
|
||||
hasMoreToLoad.value = timeline.hasMoreToLoad
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
package io.element.android.x.features.messages.timeline
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.x.architecture.Async
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class TimelineState(
|
||||
val timelineItems: Async<List<MessagesTimelineItemState>> = Async.Uninitialized,
|
||||
val hasMoreToLoad: Boolean = true,
|
||||
val highlightedEventId: EventId? = null,
|
||||
val eventSink: (TimelineEvents) -> Unit = {}
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val hasMoreToLoad: Boolean,
|
||||
val highlightedEventId: EventId?,
|
||||
val eventSink: (TimelineEvents) -> Unit
|
||||
)
|
||||
|
||||
@@ -42,32 +42,31 @@ 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.x.architecture.Async
|
||||
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.AvatarData
|
||||
import io.element.android.x.features.messages.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider
|
||||
import io.element.android.x.features.messages.model.MessagesItemReactionState
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemGroupPositionProvider
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemImageContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
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.MessagesReactionsView
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemEncryptedView
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemImageView
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemRedactedView
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemTextView
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemUnknownView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemEncryptedView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemImageView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemRedactedView
|
||||
import io.element.android.x.features.messages.timeline.components.TimelineItemTextView
|
||||
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.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -75,13 +74,11 @@ import kotlinx.coroutines.launch
|
||||
fun TimelineView(
|
||||
state: TimelineState,
|
||||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {},
|
||||
onMessageLongClicked: (MessagesTimelineItemState.MessageEvent) -> Unit = {},
|
||||
onMessageClicked: (TimelineItem.MessageEvent) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.MessageEvent) -> Unit = {},
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val timelineItems = state.timelineItems.dataOrNull().orEmpty().toImmutableList()
|
||||
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
@@ -90,7 +87,7 @@ fun TimelineView(
|
||||
reverseLayout = true
|
||||
) {
|
||||
items(
|
||||
items = timelineItems,
|
||||
items = state.timelineItems,
|
||||
contentType = { timelineItem -> timelineItem.contentType() },
|
||||
key = { timelineItem -> timelineItem.key() },
|
||||
) { timelineItem ->
|
||||
@@ -114,36 +111,36 @@ fun TimelineView(
|
||||
|
||||
TimelineScrollHelper(
|
||||
lazyListState = lazyListState,
|
||||
timelineItems = timelineItems,
|
||||
timelineItems = state.timelineItems,
|
||||
onLoadMore = ::onReachedLoadMore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessagesTimelineItemState.key(): String {
|
||||
private fun TimelineItem.key(): String {
|
||||
return when (this) {
|
||||
is MessagesTimelineItemState.MessageEvent -> id
|
||||
is MessagesTimelineItemState.Virtual -> id
|
||||
is TimelineItem.MessageEvent -> id.value
|
||||
is TimelineItem.Virtual -> id
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessagesTimelineItemState.contentType(): Int {
|
||||
private fun TimelineItem.contentType(): Int {
|
||||
return when (this) {
|
||||
is MessagesTimelineItemState.MessageEvent -> 0
|
||||
is MessagesTimelineItemState.Virtual -> 1
|
||||
is TimelineItem.MessageEvent -> 0
|
||||
is TimelineItem.Virtual -> 1
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimelineItemRow(
|
||||
timelineItem: MessagesTimelineItemState,
|
||||
timelineItem: TimelineItem,
|
||||
isHighlighted: Boolean,
|
||||
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||
onClick: (TimelineItem.MessageEvent) -> Unit,
|
||||
onLongClick: (TimelineItem.MessageEvent) -> Unit,
|
||||
) {
|
||||
when (timelineItem) {
|
||||
is MessagesTimelineItemState.Virtual -> return
|
||||
is MessagesTimelineItemState.MessageEvent -> MessageEventRow(
|
||||
is TimelineItem.Virtual -> return
|
||||
is TimelineItem.MessageEvent -> MessageEventRow(
|
||||
messageEvent = timelineItem,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = { onClick(timelineItem) },
|
||||
@@ -154,7 +151,7 @@ fun TimelineItemRow(
|
||||
|
||||
@Composable
|
||||
fun MessageEventRow(
|
||||
messageEvent: MessagesTimelineItemState.MessageEvent,
|
||||
messageEvent: TimelineItem.MessageEvent,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
@@ -197,32 +194,32 @@ fun MessageEventRow(
|
||||
) {
|
||||
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
when (messageEvent.content) {
|
||||
is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView(
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView(
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView(
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = messageEvent.content,
|
||||
interactionSource = interactionSource,
|
||||
modifier = contentModifier,
|
||||
onTextClicked = onClick,
|
||||
onTextLongClicked = onLongClick
|
||||
)
|
||||
is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView(
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView(
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = messageEvent.content,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
MessagesReactionsView(
|
||||
TimelineItemReactionsView(
|
||||
reactionsState = messageEvent.reactionsState,
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
@@ -264,7 +261,7 @@ private fun MessageSenderInformation(
|
||||
@Composable
|
||||
internal fun BoxScope.TimelineScrollHelper(
|
||||
lazyListState: LazyListState,
|
||||
timelineItems: ImmutableList<MessagesTimelineItemState>,
|
||||
timelineItems: ImmutableList<TimelineItem>,
|
||||
onLoadMore: () -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -338,8 +335,8 @@ internal fun TimelineLoadingMoreIndicator() {
|
||||
}
|
||||
|
||||
class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
|
||||
PairCombinedPreviewParameter<MessagesItemGroupPosition, MessagesTimelineItemContent>(
|
||||
MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider()
|
||||
PairCombinedPreviewParameter<MessagesItemGroupPosition, TimelineItemContent>(
|
||||
TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider()
|
||||
)
|
||||
|
||||
@Suppress("PreviewPublic")
|
||||
@@ -347,7 +344,7 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
|
||||
@Composable
|
||||
fun TimelineItemsPreview(
|
||||
@PreviewParameter(MessagesTimelineItemContentProvider::class)
|
||||
content: MessagesTimelineItemContent
|
||||
content: TimelineItemContent
|
||||
) {
|
||||
val timelineItems = persistentListOf(
|
||||
// 3 items (First Middle Last) with isMine = false
|
||||
@@ -385,23 +382,26 @@ fun TimelineItemsPreview(
|
||||
)
|
||||
TimelineView(
|
||||
state = TimelineState(
|
||||
timelineItems = Async.Success(timelineItems)
|
||||
timelineItems = timelineItems,
|
||||
hasMoreToLoad = true,
|
||||
highlightedEventId = null,
|
||||
eventSink = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMessageEvent(
|
||||
isMine: Boolean,
|
||||
content: MessagesTimelineItemContent,
|
||||
content: TimelineItemContent,
|
||||
groupPosition: MessagesItemGroupPosition
|
||||
): MessagesTimelineItemState {
|
||||
return MessagesTimelineItemState.MessageEvent(
|
||||
id = Math.random().toString(),
|
||||
): TimelineItem {
|
||||
return TimelineItem.MessageEvent(
|
||||
id = EventId(Math.random().toString()),
|
||||
senderId = "senderId",
|
||||
senderAvatar = AvatarData("sender"),
|
||||
content = content,
|
||||
reactionsState = MessagesItemReactionState(
|
||||
listOf(
|
||||
reactionsState = TimelineItemReactions(
|
||||
persistentListOf(
|
||||
AggregatedReaction("👍", "1")
|
||||
)
|
||||
),
|
||||
|
||||
@@ -36,7 +36,7 @@ import io.element.android.x.designsystem.SystemGrey5Dark
|
||||
import io.element.android.x.designsystem.SystemGrey5Light
|
||||
import io.element.android.x.designsystem.SystemGrey6Dark
|
||||
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
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
fun MessagesTimelineItemEncryptedView(
|
||||
content: MessagesTimelineItemEncryptedContent,
|
||||
fun TimelineItemEncryptedView(
|
||||
content: TimelineItemEncryptedContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
MessagesTimelineItemInformativeView(
|
||||
TimelineItemInformativeView(
|
||||
text = "Decryption error",
|
||||
iconDescription = "Warning",
|
||||
icon = Icons.Default.Warning,
|
||||
@@ -33,11 +33,11 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil.compose.AsyncImage
|
||||
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
|
||||
fun MessagesTimelineItemImageView(
|
||||
content: MessagesTimelineItemImageContent,
|
||||
fun TimelineItemImageView(
|
||||
content: TimelineItemImageContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val widthPercent = if (content.aspectRatio > 1f) {
|
||||
@@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun MessagesTimelineItemInformativeView(
|
||||
fun TimelineItemInformativeView(
|
||||
text: String,
|
||||
iconDescription: String,
|
||||
icon: ImageVector,
|
||||
@@ -32,12 +32,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import io.element.android.x.features.messages.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.model.MessagesItemReactionState
|
||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItemReactions
|
||||
|
||||
@Composable
|
||||
fun MessagesReactionsView(
|
||||
reactionsState: MessagesItemReactionState,
|
||||
fun TimelineItemReactionsView(
|
||||
reactionsState: TimelineItemReactions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (reactionsState.reactions.isEmpty()) return
|
||||
@@ -20,15 +20,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||
import io.element.android.x.features.messages.timeline.components.MessagesTimelineItemInformativeView
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemRedactedContent
|
||||
|
||||
@Composable
|
||||
fun MessagesTimelineItemRedactedView(
|
||||
content: MessagesTimelineItemRedactedContent,
|
||||
fun TimelineItemRedactedView(
|
||||
content: TimelineItemRedactedContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
MessagesTimelineItemInformativeView(
|
||||
TimelineItemInformativeView(
|
||||
text = "This message has been deleted",
|
||||
iconDescription = "Delete",
|
||||
icon = Icons.Default.Delete,
|
||||
@@ -31,11 +31,11 @@ import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.x.designsystem.LinkColor
|
||||
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.model.content.MessagesTimelineItemTextBasedContent
|
||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemTextBasedContent
|
||||
|
||||
@Composable
|
||||
fun MessagesTimelineItemTextView(
|
||||
content: MessagesTimelineItemTextBasedContent,
|
||||
fun TimelineItemTextView(
|
||||
content: TimelineItemTextBasedContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextClicked: () -> Unit = {},
|
||||
@@ -20,14 +20,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
fun MessagesTimelineItemUnknownView(
|
||||
content: MessagesTimelineItemUnknownContent,
|
||||
fun TimelineItemUnknownView(
|
||||
content: TimelineItemUnknownContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
MessagesTimelineItemInformativeView(
|
||||
TimelineItemInformativeView(
|
||||
text = "Event not handled by EAX",
|
||||
iconDescription = "Info",
|
||||
icon = Icons.Default.Info,
|
||||
@@ -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.x.features.messages.diff
|
||||
package io.element.android.x.features.messages.timeline.diff
|
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import io.element.android.x.features.messages.util.invalidateLast
|
||||
import io.element.android.x.features.messages.timeline.model.TimelineItem
|
||||
import io.element.android.x.features.messages.timeline.util.invalidateLast
|
||||
import timber.log.Timber
|
||||
|
||||
internal class CacheInvalidator(private val itemStatesCache: MutableList<MessagesTimelineItemState?>) :
|
||||
internal class CacheInvalidator(private val itemStatesCache: MutableList<TimelineItem?>) :
|
||||
ListUpdateCallback {
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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 io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
@@ -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,27 +14,30 @@
|
||||
* 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.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(
|
||||
val id: String
|
||||
) : MessagesTimelineItemState
|
||||
) : TimelineItem
|
||||
|
||||
data class MessageEvent(
|
||||
val id: String,
|
||||
val id: EventId,
|
||||
val senderId: String,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatar: AvatarData,
|
||||
val content: MessagesTimelineItemContent,
|
||||
val content: TimelineItemContent,
|
||||
val sentTime: String = "",
|
||||
val isMine: Boolean = false,
|
||||
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
|
||||
val reactionsState: MessagesItemReactionState
|
||||
) : MessagesTimelineItemState {
|
||||
val reactionsState: TimelineItemReactions
|
||||
) : TimelineItem {
|
||||
|
||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||
|
||||
@@ -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,10 +14,12 @@
|
||||
* 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
|
||||
|
||||
@Immutable
|
||||
sealed interface MessagesItemGroupPosition {
|
||||
object First : 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(
|
||||
MessagesItemGroupPosition.First,
|
||||
MessagesItemGroupPosition.Middle,
|
||||
@@ -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,16 +14,14 @@
|
||||
* 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 MessagesItemReactionState(
|
||||
val reactions: List<AggregatedReaction>
|
||||
data class TimelineItemReactions(
|
||||
val reactions: ImmutableList<AggregatedReaction>
|
||||
)
|
||||
|
||||
@Stable
|
||||
data class AggregatedReaction(
|
||||
val key: String,
|
||||
val count: String,
|
||||
@@ -14,32 +14,32 @@
|
||||
* 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 org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
sealed interface MessagesTimelineItemContent
|
||||
sealed interface TimelineItemContent
|
||||
|
||||
class MessagesTimelineItemContentProvider : PreviewParameterProvider<MessagesTimelineItemContent> {
|
||||
class MessagesTimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
|
||||
override val values = sequenceOf(
|
||||
MessagesTimelineItemEmoteContent(
|
||||
TimelineItemEmoteContent(
|
||||
body = "Emote",
|
||||
htmlDocument = null
|
||||
),
|
||||
MessagesTimelineItemEncryptedContent(
|
||||
TimelineItemEncryptedContent(
|
||||
encryptedMessage = EncryptedMessage.Unknown
|
||||
),
|
||||
// TODO MessagesTimelineItemImageContent(),
|
||||
MessagesTimelineItemNoticeContent(
|
||||
TimelineItemNoticeContent(
|
||||
body = "Notice",
|
||||
htmlDocument = null
|
||||
),
|
||||
MessagesTimelineItemRedactedContent,
|
||||
MessagesTimelineItemTextContent(
|
||||
TimelineItemRedactedContent,
|
||||
TimelineItemTextContent(
|
||||
body = "Text",
|
||||
htmlDocument = null
|
||||
),
|
||||
MessagesTimelineItemUnknownContent,
|
||||
TimelineItemUnknownContent,
|
||||
)
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
* 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
|
||||
|
||||
data class MessagesTimelineItemTextContent(
|
||||
data class TimelineItemEmoteContent(
|
||||
override val body: String,
|
||||
override val htmlDocument: Document?
|
||||
) : MessagesTimelineItemTextBasedContent
|
||||
) : TimelineItemTextBasedContent
|
||||
@@ -14,10 +14,10 @@
|
||||
* 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
|
||||
|
||||
data class MessagesTimelineItemEncryptedContent(
|
||||
data class TimelineItemEncryptedContent(
|
||||
val encryptedMessage: EncryptedMessage
|
||||
) : MessagesTimelineItemContent
|
||||
) : TimelineItemContent
|
||||
@@ -14,13 +14,13 @@
|
||||
* 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
|
||||
|
||||
data class MessagesTimelineItemImageContent(
|
||||
data class TimelineItemImageContent(
|
||||
val body: String,
|
||||
val imageMeta: MediaResolver.Meta,
|
||||
val blurhash: String?,
|
||||
val aspectRatio: Float
|
||||
) : MessagesTimelineItemContent
|
||||
) : TimelineItemContent
|
||||
@@ -14,11 +14,11 @@
|
||||
* 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
|
||||
|
||||
data class MessagesTimelineItemEmoteContent(
|
||||
data class TimelineItemNoticeContent(
|
||||
override val body: String,
|
||||
override val htmlDocument: Document?
|
||||
) : MessagesTimelineItemTextBasedContent
|
||||
) : TimelineItemTextBasedContent
|
||||
@@ -14,6 +14,6 @@
|
||||
* 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
|
||||
@@ -14,11 +14,11 @@
|
||||
* 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
|
||||
|
||||
sealed interface MessagesTimelineItemTextBasedContent : MessagesTimelineItemContent {
|
||||
sealed interface TimelineItemTextBasedContent : TimelineItemContent {
|
||||
val body: String
|
||||
val htmlDocument: Document?
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
* 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
|
||||
|
||||
data class MessagesTimelineItemNoticeContent(
|
||||
data class TimelineItemTextContent(
|
||||
override val body: String,
|
||||
override val htmlDocument: Document?
|
||||
) : MessagesTimelineItemTextBasedContent
|
||||
) : TimelineItemTextBasedContent
|
||||
@@ -14,6 +14,6 @@
|
||||
* 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
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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() {
|
||||
val indexOfLast = size
|
||||
@@ -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.sessionId
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationService
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class Matrix @Inject constructor(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
@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 sessionStore = SessionStore(context)
|
||||
private val authService = AuthenticationService(baseDirectory.absolutePath)
|
||||
@@ -57,7 +50,7 @@ class Matrix @Inject constructor(
|
||||
return sessionStore.isLoggedIn()
|
||||
}
|
||||
|
||||
suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io){
|
||||
suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
|
||||
sessionStore.getLatestSession()?.sessionId()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.x.matrix.room
|
||||
|
||||
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.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -39,13 +40,15 @@ class MatrixRoom(
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
fun syncUpdateFlow(): Flow<Unit> {
|
||||
fun syncUpdateFlow(): Flow<Long> {
|
||||
return slidingSyncUpdateFlow
|
||||
.filter {
|
||||
it.rooms.contains(room.id())
|
||||
}
|
||||
.map { }
|
||||
.onStart { emit(Unit) }
|
||||
.map {
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
.onStart { emit(System.currentTimeMillis()) }
|
||||
}
|
||||
|
||||
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 content = messageEventContentFromMarkdown(message)
|
||||
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 content = messageEventContentFromMarkdown(message)
|
||||
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()
|
||||
runCatching {
|
||||
room.redact(eventId, reason, transactionId)
|
||||
room.redact(eventId.value, reason, transactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.x.matrix.timeline
|
||||
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -146,11 +147,11 @@ class MatrixTimeline(
|
||||
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)
|
||||
}
|
||||
|
||||
suspend fun replyMessage(inReplyToEventId: String, message: String): Result<Unit> {
|
||||
suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
|
||||
return matrixRoom.replyMessage(inReplyToEventId, message)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ import io.element.android.x.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
class MatrixItemHelper(
|
||||
class MatrixItemHelper @Inject constructor(
|
||||
private val client: MatrixClient
|
||||
) {
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":libraries:elementresources"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:matrix"))
|
||||
implementation(libs.wysiwyg)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
|
||||
@@ -17,31 +17,32 @@
|
||||
package io.element.android.x.textcomposer
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.x.matrix.core.EventId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface MessageComposerMode : Parcelable {
|
||||
@Parcelize
|
||||
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
|
||||
|
||||
@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)
|
||||
|
||||
@Parcelize
|
||||
class Quote(override val eventId: String, override val defaultContent: CharSequence) :
|
||||
class Quote(override val eventId: EventId, override val defaultContent: CharSequence) :
|
||||
Special(eventId, defaultContent)
|
||||
|
||||
@Parcelize
|
||||
class Reply(
|
||||
val senderName: String,
|
||||
override val eventId: String,
|
||||
override val eventId: EventId,
|
||||
override val defaultContent: CharSequence
|
||||
) : Special(eventId, defaultContent)
|
||||
|
||||
val relatedEventId: String?
|
||||
val relatedEventId: EventId?
|
||||
get() = when (this) {
|
||||
is Normal -> null
|
||||
is Edit -> eventId
|
||||
|
||||
Reference in New Issue
Block a user