element-x-android/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt

234 lines
8.0 KiB
Kotlin

package io.element.android.x.features.messages
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.model.MessagesItemAction
import io.element.android.x.features.messages.model.MessagesItemActionsSheetState
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.textcomposer.MessageComposerMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
private const val PAGINATION_COUNT = 50
class MessagesViewModel(
private val client: MatrixClient,
private val room: MatrixRoom,
private val timeline: MatrixTimeline,
private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory,
private val initialState: MessagesViewState
) :
MavericksViewModel<MessagesViewState>(initialState) {
companion object : MavericksViewModelFactory<MessagesViewModel, MessagesViewState> {
override fun create(
viewModelContext: ViewModelContext,
state: MessagesViewState
): MessagesViewModel? {
val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient()
val room = client.getRoom(state.roomId) ?: return null
val messageTimelineItemStateFactory =
MessageTimelineItemStateFactory(client, room, Dispatchers.Default)
return MessagesViewModel(
client,
room,
room.timeline(),
messageTimelineItemStateFactory,
state
)
}
}
private val timelineCallback = object : MatrixTimeline.Callback {
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
viewModelScope.launch {
messageTimelineItemStateFactory.pushItem(timelineItem)
}
}
}
init {
handleInit()
}
fun loadMore() {
viewModelScope.launch {
timeline.paginateBackwards(PAGINATION_COUNT)
setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) }
}
}
fun sendMessage(text: String) {
viewModelScope.launch {
val state = awaitState()
// Reset composer right away
setNormalMode()
when (state.composerMode) {
is MessageComposerMode.Normal -> timeline.sendMessage(text)
is MessageComposerMode.Edit -> timeline.editMessage(
state.composerMode.eventId,
text
)
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> timeline.replyMessage(
state.composerMode.eventId,
text
)
}
}
}
suspend fun getTargetEvent(): MessagesTimelineItemState.MessageEvent? {
val currentState = awaitState()
return currentState.itemActionsSheetState.invoke()?.targetItem
}
fun handleItemAction(
action: MessagesItemAction,
targetEvent: MessagesTimelineItemState.MessageEvent
) {
viewModelScope.launch(Dispatchers.Default) {
when (action) {
MessagesItemAction.Copy -> notImplementedYet()
MessagesItemAction.Forward -> notImplementedYet()
MessagesItemAction.Redact -> handleActionRedact(targetEvent)
MessagesItemAction.Edit -> handleActionEdit(targetEvent)
MessagesItemAction.Reply -> handleActionReply(targetEvent)
}
}
}
fun setNormalMode() {
setComposerMode(MessageComposerMode.Normal(""))
}
fun onSnackbarShown() {
setSnackbarContent(null)
}
fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent?) {
if (messagesTimelineItemState == null) {
setState { copy(itemActionsSheetState = Uninitialized) }
return
}
suspend {
val actions =
if (messagesTimelineItemState.content is MessagesTimelineItemRedactedContent) {
emptyList()
} else {
mutableListOf(
MessagesItemAction.Reply,
MessagesItemAction.Forward,
MessagesItemAction.Copy,
).also {
if (messagesTimelineItemState.isMine) {
it.add(MessagesItemAction.Edit)
it.add(MessagesItemAction.Redact)
}
}
}
MessagesItemActionsSheetState(
targetItem = messagesTimelineItemState,
actions = actions
)
}.execute(Dispatchers.Default) {
copy(itemActionsSheetState = it)
}
}
private fun handleInit() {
timeline.initialize()
timeline.callback = timelineCallback
room.syncUpdateFlow()
.onEach {
val avatarData =
loadAvatarData(room.name ?: room.roomId.value, room.avatarUrl, AvatarSize.SMALL)
setState {
copy(
roomName = room.name, roomAvatar = avatarData,
)
}
}.launchIn(viewModelScope)
timeline
.timelineItems()
.onEach(messageTimelineItemStateFactory::replaceWith)
.launchIn(viewModelScope)
messageTimelineItemStateFactory
.flow()
.execute {
copy(timelineItems = it)
}
}
private fun setSnackbarContent(message: String?) {
setState { copy(snackbarContent = message) }
}
private fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) {
viewModelScope.launch {
room.redactEvent(event.id)
}
}
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(
MessageComposerMode.Edit(
targetEvent.id,
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
)
)
}
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, ""))
}
private fun setComposerMode(mode: MessageComposerMode) {
setState {
copy(
composerMode = mode,
highlightedEventId = mode.relatedEventId
)
}
}
private fun notImplementedYet() {
setSnackbarContent("Not implemented yet!")
}
private suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
override fun onCleared() {
super.onCleared()
timeline.callback = null
timeline.dispose()
}
}