Finish migration of Messages screen
This commit is contained in:
parent
2869f492d9
commit
7b197e6e8b
47 changed files with 354 additions and 315 deletions
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
MessagesView(
|
MessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = this::navigateUp,
|
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.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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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?) {
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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?
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue