mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Text composer: start extracting in a dedicated ViewModel/State/Action/Events
This commit is contained in:
parent
7c2fac39f4
commit
9815dfe449
9 changed files with 770 additions and 589 deletions
|
@ -30,10 +30,7 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
|
|||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
data class SaveDraft(val draft: String) : RoomDetailAction()
|
||||
data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction()
|
||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
||||
data class SendMedia(val attachments: List<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
|
||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
||||
|
@ -52,11 +49,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
object EnterTrackingUnreadMessagesState : RoomDetailAction()
|
||||
object ExitTrackingUnreadMessagesState : RoomDetailAction()
|
||||
|
||||
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : RoomDetailAction()
|
||||
|
||||
data class ResendMessage(val eventId: String) : RoomDetailAction()
|
||||
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
|
||||
data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction()
|
||||
|
|
|
@ -133,7 +133,11 @@ import im.vector.app.features.command.Command
|
|||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerAction
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView
|
||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
|
@ -235,6 +239,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val permalinkHandler: PermalinkHandler,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||
val textComposerViewModelFactory: TextComposerViewModel.Factory,
|
||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val colorProvider: ColorProvider,
|
||||
|
@ -287,6 +292,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
autoCompleterFactory.create(roomDetailArgs.roomId)
|
||||
}
|
||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||
private val debouncer = Debouncer(createUIHandler())
|
||||
|
||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
|
@ -379,7 +385,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
updateJumpToReadMarkerViewVisibility()
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend ->
|
||||
textComposerViewModel.selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend ->
|
||||
if (!canSend) {
|
||||
return@selectSubscribe
|
||||
}
|
||||
|
@ -391,6 +397,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::syncState,
|
||||
RoomDetailViewState::incrementalSyncStatus,
|
||||
|
@ -404,6 +411,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
textComposerViewModel.observeViewEvents {
|
||||
when(it){
|
||||
is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||
is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
||||
is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
||||
is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
roomDetailViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is RoomDetailViewEvents.Failure -> {
|
||||
|
@ -418,8 +434,6 @@ class RoomDetailFragment @Inject constructor(
|
|||
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
||||
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
||||
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
|
||||
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
||||
is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
|
||||
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
|
||||
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
|
||||
|
@ -444,7 +458,6 @@ class RoomDetailFragment @Inject constructor(
|
|||
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
||||
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
||||
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
||||
is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -495,7 +508,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
|
||||
}
|
||||
|
||||
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) {
|
||||
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) {
|
||||
val tag = MigrateRoomBottomSheet::javaClass.name
|
||||
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
|
||||
.show(parentFragmentManager, tag)
|
||||
|
@ -753,7 +766,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) {
|
||||
private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) {
|
||||
updateComposerText("")
|
||||
lockSendButton = false
|
||||
navigator.openRoom(vectorBaseActivity, action.roomId)
|
||||
|
@ -762,7 +775,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private fun handleShareData() {
|
||||
when (val sharedData = roomDetailArgs.sharedData) {
|
||||
is SharedData.Text -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
||||
}
|
||||
is SharedData.Attachments -> {
|
||||
// open share edition
|
||||
|
@ -980,9 +993,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
private fun renderRegularMode(text: String) {
|
||||
autoCompleter.exitSpecialMode()
|
||||
views.composerLayout.collapse()
|
||||
|
||||
views.voiceMessageRecorderView.isVisible = text.isBlank()
|
||||
|
||||
updateComposerText(text)
|
||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
||||
}
|
||||
|
@ -1077,7 +1088,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
notificationDrawerManager.setCurrentRoom(null)
|
||||
|
||||
roomDetailViewModel.handle(RoomDetailAction.SaveDraft(views.composerLayout.text.toString()))
|
||||
textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
||||
|
||||
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions)
|
||||
|
@ -1196,12 +1207,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
||||
val eventId = it.eventId
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(eventId, views.composerLayout.text.toString()))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||
val canSendMessage = withState(roomDetailViewModel) {
|
||||
val canSendMessage = withState(textComposerViewModel) {
|
||||
it.canSendMessage
|
||||
}
|
||||
if (!canSendMessage) {
|
||||
|
@ -1303,7 +1314,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCloseRelatedMessage() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(views.composerLayout.text.toString(), false))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false))
|
||||
}
|
||||
|
||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||
|
@ -1320,6 +1331,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
views.voiceMessageRecorderView.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1332,7 +1344,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
// We collapse ASAP, if not there will be a slight annoying delay
|
||||
views.composerLayout.collapse(true)
|
||||
lockSendButton = true
|
||||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||
textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||
emojiPopup.dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -1344,7 +1356,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
.map { it.isNotEmpty() }
|
||||
.subscribe {
|
||||
Timber.d("Typing: User is typing: $it")
|
||||
roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it))
|
||||
textComposerViewModel.handle(TextComposerAction.UserIsTyping(it))
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
|
@ -1364,24 +1376,24 @@ class RoomDetailFragment @Inject constructor(
|
|||
return isHandled
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(roomDetailViewModel) { state ->
|
||||
override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState ->
|
||||
invalidateOptionsMenu()
|
||||
val summary = state.asyncRoomSummary()
|
||||
renderToolbar(summary, state.typingMessage)
|
||||
views.removeJitsiWidgetView.render(state)
|
||||
if (state.hasFailedSending) {
|
||||
val summary = mainState.asyncRoomSummary()
|
||||
renderToolbar(summary, mainState.formattedTypingUsers)
|
||||
views.removeJitsiWidgetView.render(mainState)
|
||||
if (mainState.hasFailedSending) {
|
||||
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true
|
||||
} else {
|
||||
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false
|
||||
}
|
||||
val inviter = state.asyncInviter()
|
||||
val inviter = mainState.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
views.jumpToBottomView.count = summary.notificationCount
|
||||
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
|
||||
timelineEventController.update(state)
|
||||
timelineEventController.update(mainState)
|
||||
lazyLoadedViews.inviteView(false)?.isVisible = false
|
||||
if (state.tombstoneEvent == null) {
|
||||
if (state.canSendMessage) {
|
||||
if (mainState.tombstoneEvent == null) {
|
||||
if (textComposerState.canSendMessage) {
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
views.composerLayout.isVisible = true
|
||||
views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse()
|
||||
|
@ -1390,30 +1402,32 @@ class RoomDetailFragment @Inject constructor(
|
|||
views.composerLayout.alwaysShowSendButton = false
|
||||
}
|
||||
} else {
|
||||
views.composerLayout.isVisible = false
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
views.hideComposerViews()
|
||||
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
||||
}
|
||||
} else {
|
||||
views.composerLayout.isVisible = false
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
|
||||
views.hideComposerViews()
|
||||
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent))
|
||||
}
|
||||
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||
views.composerLayout.isVisible = false
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
views.hideComposerViews()
|
||||
lazyLoadedViews.inviteView(true)?.apply {
|
||||
callback = this@RoomDetailFragment
|
||||
isVisible = true
|
||||
render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
|
||||
render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState)
|
||||
setOnClickListener { }
|
||||
}
|
||||
Unit
|
||||
} else if (state.asyncInviter.complete) {
|
||||
} else if (mainState.asyncInviter.complete) {
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun FragmentRoomDetailBinding.hideComposerViews() {
|
||||
composerLayout.isVisible = false
|
||||
voiceMessageRecorderView.isVisible = false
|
||||
}
|
||||
|
||||
private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
|
||||
if (roomSummary == null) {
|
||||
views.roomToolbarContentView.isClickable = false
|
||||
|
@ -1442,24 +1456,24 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) {
|
||||
private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) {
|
||||
when (sendMessageResult) {
|
||||
is RoomDetailViewEvents.SlashCommandHandled -> {
|
||||
is TextComposerViewEvents.SlashCommandHandled -> {
|
||||
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
||||
}
|
||||
is RoomDetailViewEvents.SlashCommandError -> {
|
||||
is TextComposerViewEvents.SlashCommandError -> {
|
||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||
}
|
||||
is RoomDetailViewEvents.SlashCommandUnknown -> {
|
||||
is TextComposerViewEvents.SlashCommandUnknown -> {
|
||||
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||
}
|
||||
is RoomDetailViewEvents.SlashCommandResultOk -> {
|
||||
is TextComposerViewEvents.SlashCommandResultOk -> {
|
||||
updateComposerText("")
|
||||
}
|
||||
is RoomDetailViewEvents.SlashCommandResultError -> {
|
||||
is TextComposerViewEvents.SlashCommandResultError -> {
|
||||
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
||||
}
|
||||
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
|
||||
is TextComposerViewEvents.SlashCommandNotImplemented -> {
|
||||
displayCommandError(getString(R.string.not_implemented))
|
||||
}
|
||||
} // .exhaustive
|
||||
|
@ -1938,17 +1952,17 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
is EventSharedAction.Edit -> {
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
} else {
|
||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||
}
|
||||
}
|
||||
is EventSharedAction.Quote -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
|
||||
}
|
||||
is EventSharedAction.Reply -> {
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
||||
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
||||
} else {
|
||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||
}
|
||||
|
@ -2160,7 +2174,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||
super.onContactAttachmentReady(contactAttachment)
|
||||
val formattedContact = contactAttachment.toHumanReadable()
|
||||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
|
||||
textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false))
|
||||
}
|
||||
|
||||
private fun onViewWidgetsClicked() {
|
||||
|
|
|
@ -66,8 +66,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
val mimeType: String?
|
||||
) : RoomDetailViewEvents()
|
||||
|
||||
abstract class SendMessageResult : RoomDetailViewEvents()
|
||||
|
||||
data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents()
|
||||
|
||||
object DisplayPromptForIntegrationManager : RoomDetailViewEvents()
|
||||
|
@ -82,19 +80,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
val domain: String,
|
||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents()
|
||||
|
||||
object MessageSent : SendMessageResult()
|
||||
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
|
||||
class SlashCommandError(val command: Command) : SendMessageResult()
|
||||
class SlashCommandUnknown(val command: String) : SendMessageResult()
|
||||
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
|
||||
object SlashCommandResultOk : SendMessageResult()
|
||||
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
|
||||
|
||||
// TODO Remove
|
||||
object SlashCommandNotImplemented : SendMessageResult()
|
||||
|
||||
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||
object StopChatEffects : RoomDetailViewEvents()
|
||||
object RoomReplacementStarted : RoomDetailViewEvents()
|
||||
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean): RoomDetailViewEvents()
|
||||
}
|
||||
|
|
|
@ -44,8 +44,6 @@ import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
|||
import im.vector.app.features.call.conference.JitsiService
|
||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.command.CommandParser
|
||||
import im.vector.app.features.command.ParsedCommand
|
||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
|
@ -67,9 +65,6 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
|
@ -86,22 +81,14 @@ import org.matrix.android.sdk.api.session.initsync.SyncStatusService
|
|||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.read.ReadService
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
|
@ -181,7 +168,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
observeSyncState()
|
||||
observeDataStore()
|
||||
observeEventDisplayedActions()
|
||||
loadDraftIfAny()
|
||||
observeUnreadState()
|
||||
observeMyRoomMember()
|
||||
observeActiveRoomWidgets()
|
||||
|
@ -235,13 +221,11 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private fun observePowerLevel() {
|
||||
PowerLevelsObservableFactory(room).createObservable()
|
||||
.subscribe {
|
||||
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
|
||||
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
|
||||
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
|
||||
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
|
||||
setState {
|
||||
copy(
|
||||
canSendMessage = canSendMessage,
|
||||
canInvite = canInvite,
|
||||
isAllowedToManageWidgets = isAllowedToManageWidgets,
|
||||
isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall
|
||||
|
@ -300,10 +284,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
|
@ -315,10 +296,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom()
|
||||
|
@ -590,70 +567,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
return room.getRoomMember(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a send mode to a draft and save the draft
|
||||
*/
|
||||
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState {
|
||||
session.coroutineScope.launch {
|
||||
when {
|
||||
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
|
||||
room.saveDraft(UserDraft.REGULAR(action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.REPLY -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.QUOTE -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.EDIT -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDraftIfAny() {
|
||||
val currentDraft = room.getDraft()
|
||||
setState {
|
||||
copy(
|
||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||
sendMode = when (currentDraft) {
|
||||
is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false)
|
||||
is UserDraft.QUOTE -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.QUOTE(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.REPLY -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.REPLY(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.EDIT -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.EDIT(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: SendMode.REGULAR("", fromSharing = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) {
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
if (action.isTyping) {
|
||||
room.userIsTyping()
|
||||
} else {
|
||||
room.userStopsTyping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
|
||||
// Ensure outbound session keys
|
||||
if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
|
||||
|
@ -768,416 +681,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
is SendMode.REGULAR -> {
|
||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/"))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
|
||||
}
|
||||
is ParsedCommand.SendPlainText -> {
|
||||
// Send the text message to the room, without markdown
|
||||
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.Invite3Pid -> {
|
||||
handleInvite3pidSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
handleSetUserPowerLevel(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled(
|
||||
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
handleUnbanSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
handleBanSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
handleKickSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
handleJoinToAnotherRoomSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbow -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbowEmote -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendSpoiler -> {
|
||||
room.sendFormattedTextMessage(
|
||||
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
|
||||
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
|
||||
)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendShrug -> {
|
||||
val sequence = buildString {
|
||||
append("¯\\_(ツ)_/¯")
|
||||
if (slashCommandResult.message.isNotEmpty()) {
|
||||
append(" ")
|
||||
append(slashCommandResult.message)
|
||||
}
|
||||
}
|
||||
room.sendTextMessage(sequence)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendChatEffect -> {
|
||||
sendChatEffect(slashCommandResult)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendPoll -> {
|
||||
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
handleChangeDisplayNameSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.DiscardSession -> {
|
||||
if (room.isEncrypted()) {
|
||||
session.cryptoService().discardOutboundSession(room.roomId)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
} else {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents
|
||||
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
|
||||
)
|
||||
}
|
||||
}
|
||||
is ParsedCommand.CreateSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val params = CreateSpaceParams().apply {
|
||||
name = slashCommandResult.name
|
||||
invitedUserIds.addAll(slashCommandResult.invitees)
|
||||
}
|
||||
val spaceId = session.spaceService().createSpace(params)
|
||||
session.spaceService().getSpace(spaceId)
|
||||
?.addChildren(
|
||||
state.roomId,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.AddToSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.spaceService().getSpace(slashCommandResult.spaceId)
|
||||
?.addChildren(
|
||||
room.roomId,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.JoinSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.LeaveRoom -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.getRoom(slashCommandResult.roomId)?.leave(null)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.UpgradeRoom -> {
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents.ShowRoomUpgradeDialog(
|
||||
slashCommandResult.newVersion,
|
||||
room.roomSummary()?.isPublic ?: false
|
||||
)
|
||||
)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
// TODO check if same content?
|
||||
room.getTimeLineEvent(inReplyTo)?.let {
|
||||
room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
|
||||
}
|
||||
} else {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val existingBody = messageContent?.body ?: ""
|
||||
if (existingBody != action.text) {
|
||||
room.editTextMessage(state.sendMode.timelineEvent,
|
||||
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
||||
action.text,
|
||||
action.autoMarkdown)
|
||||
} else {
|
||||
Timber.w("Same message content, do not send edition")
|
||||
}
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
||||
|
||||
// TODO check for pills?
|
||||
|
||||
// TODO Refactor this, just temporary for quotes
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(finalText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (finalText == htmlText) {
|
||||
room.sendTextMessage(finalText)
|
||||
} else {
|
||||
room.sendFormattedTextMessage(finalText, htmlText)
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) {
|
||||
// If message is blank, convert to an emote, with default message
|
||||
if (sendChatEffect.message.isBlank()) {
|
||||
val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) {
|
||||
ChatEffect.CONFETTI -> R.string.default_message_emote_confetti
|
||||
ChatEffect.SNOWFALL -> R.string.default_message_emote_snow
|
||||
})
|
||||
room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE)
|
||||
} else {
|
||||
room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType())
|
||||
}
|
||||
}
|
||||
|
||||
private fun popDraft() = withState {
|
||||
if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) {
|
||||
// If we were sharing, we want to get back our last value from draft
|
||||
loadDraftIfAny()
|
||||
} else {
|
||||
// Otherwise we clear the composer and remove the draft from db
|
||||
setState { copy(sendMode = SendMode.REGULAR("", false)) }
|
||||
viewModelScope.launch {
|
||||
room.deleteDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
session.joinRoom(command.roomAlias, command.reason, emptyList())
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
return@launch
|
||||
}
|
||||
session.getRoomSummary(command.roomAlias)
|
||||
?.roomId
|
||||
?.let {
|
||||
_viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||
return buildString {
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].isNotBlank()) {
|
||||
append("> ")
|
||||
append(messageParagraphs[i])
|
||||
}
|
||||
|
||||
if (i != messageParagraphs.lastIndex) {
|
||||
append("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
append("\n\n")
|
||||
append(myText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.updateTopic(changeTopic.topic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.invite(invite.userId, invite.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.invite3pid(invite.threePid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
|
||||
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
||||
?.content
|
||||
?.toModel<PowerLevelsContent>()
|
||||
?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
|
||||
?.toContent()
|
||||
?: return
|
||||
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
session.setDisplayName(session.myUserId, changeDisplayName.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.kick(kick.userId, kick.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.ban(ban.userId, ban.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.unban(unban.userId, unban.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSlashCommandFlow(lambda: (MatrixCallback<Unit>) -> Unit) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
val matrixCallback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
lambda.invoke(matrixCallback)
|
||||
}
|
||||
|
||||
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
viewModelScope.launch {
|
||||
val event = try {
|
||||
block()
|
||||
RoomDetailViewEvents.SlashCommandResultOk
|
||||
} catch (failure: Exception) {
|
||||
RoomDetailViewEvents.SlashCommandResultError(failure)
|
||||
}
|
||||
_viewEvents.post(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendReaction(action: RoomDetailAction.SendReaction) {
|
||||
room.sendReaction(action.targetEventId, action.reaction)
|
||||
}
|
||||
|
@ -1246,28 +749,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleEditAction(action: RoomDetailAction.EnterEditMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnterRegularMode(action: RoomDetailAction.EnterRegularMode) = setState {
|
||||
copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
|
||||
}
|
||||
|
||||
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
|
||||
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||
val isLocalSendingFile = action.senderId == session.myUserId
|
||||
|
@ -1604,7 +1085,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
setState {
|
||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||
copy(
|
||||
typingMessage = typingMessage,
|
||||
formattedTypingUsers = typingMessage,
|
||||
hasFailedSending = summary.hasFailedSending
|
||||
)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ data class RoomDetailViewState(
|
|||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
||||
val typingMessage: String? = null,
|
||||
val formattedTypingUsers: String? = null,
|
||||
val sendMode: SendMode = SendMode.REGULAR("", false),
|
||||
val tombstoneEvent: Event? = null,
|
||||
val joinUpgradedRoomAsync: Async<String> = Uninitialized,
|
||||
|
@ -84,7 +84,6 @@ data class RoomDetailViewState(
|
|||
val unreadState: UnreadState = UnreadState.Unknown,
|
||||
val canShowJumpToReadMarker: Boolean = true,
|
||||
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
|
||||
val canSendMessage: Boolean = true,
|
||||
val canInvite: Boolean = true,
|
||||
val isAllowedToManageWidgets: Boolean = false,
|
||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
|
||||
sealed class TextComposerAction : VectorViewModelAction {
|
||||
data class SaveDraft(val draft: String) : TextComposerAction()
|
||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : TextComposerAction()
|
||||
data class EnterEditMode(val eventId: String, val text: String) : TextComposerAction()
|
||||
data class EnterQuoteMode(val eventId: String, val text: String) : TextComposerAction()
|
||||
data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction()
|
||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction()
|
||||
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.command.Command
|
||||
|
||||
sealed class TextComposerViewEvents : VectorViewEvents {
|
||||
|
||||
data class ShowMessage(val message: String) : TextComposerViewEvents()
|
||||
|
||||
abstract class SendMessageResult : TextComposerViewEvents()
|
||||
|
||||
object MessageSent : SendMessageResult()
|
||||
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
|
||||
class SlashCommandError(val command: Command) : SendMessageResult()
|
||||
class SlashCommandUnknown(val command: String) : SendMessageResult()
|
||||
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
|
||||
object SlashCommandResultOk : SendMessageResult()
|
||||
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
|
||||
|
||||
// TODO Remove
|
||||
object SlashCommandNotImplemented : SendMessageResult()
|
||||
|
||||
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents()
|
||||
}
|
|
@ -0,0 +1,603 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.command.CommandParser
|
||||
import im.vector.app.features.command.ParsedCommand
|
||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||
import im.vector.app.features.home.room.detail.ChatEffect
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
||||
import im.vector.app.features.home.room.detail.SendMode
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.toMessageType
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
|
||||
import timber.log.Timber
|
||||
|
||||
class TextComposerViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: TextComposerViewState,
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val rainbowGenerator: RainbowGenerator
|
||||
) : VectorViewModel<TextComposerViewState, TextComposerAction, TextComposerViewEvents>(initialState) {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
|
||||
init {
|
||||
loadDraftIfAny()
|
||||
observePowerLevel()
|
||||
}
|
||||
|
||||
override fun handle(action: TextComposerAction) {
|
||||
Timber.v("Handle action: $action")
|
||||
when (action) {
|
||||
is TextComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
||||
is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||
is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
||||
is TextComposerAction.SaveDraft -> handleSaveDraft(action)
|
||||
is TextComposerAction.SendMessage -> handleSendMessage(action)
|
||||
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState {
|
||||
copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
|
||||
}
|
||||
|
||||
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePowerLevel() {
|
||||
PowerLevelsObservableFactory(room).createObservable()
|
||||
.subscribe {
|
||||
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
|
||||
setState {
|
||||
copy(canSendMessage = canSendMessage)
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleEnterQuoteMode(action: TextComposerAction.EnterQuoteMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnterReplyMode(action: TextComposerAction.EnterReplyMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendMessage(action: TextComposerAction.SendMessage) {
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
is SendMode.REGULAR -> {
|
||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/"))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
|
||||
}
|
||||
is ParsedCommand.SendPlainText -> {
|
||||
// Send the text message to the room, without markdown
|
||||
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.Invite3Pid -> {
|
||||
handleInvite3pidSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
handleSetUserPowerLevel(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled(
|
||||
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
handleUnbanSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
handleBanSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
handleKickSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
handleJoinToAnotherRoomSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbow -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbowEmote -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendSpoiler -> {
|
||||
room.sendFormattedTextMessage(
|
||||
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
|
||||
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
|
||||
)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendShrug -> {
|
||||
val sequence = buildString {
|
||||
append("¯\\_(ツ)_/¯")
|
||||
if (slashCommandResult.message.isNotEmpty()) {
|
||||
append(" ")
|
||||
append(slashCommandResult.message)
|
||||
}
|
||||
}
|
||||
room.sendTextMessage(sequence)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendChatEffect -> {
|
||||
sendChatEffect(slashCommandResult)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendPoll -> {
|
||||
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
handleChangeDisplayNameSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.DiscardSession -> {
|
||||
if (room.isEncrypted()) {
|
||||
session.cryptoService().discardOutboundSession(room.roomId)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
} else {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
_viewEvents.post(
|
||||
TextComposerViewEvents
|
||||
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
|
||||
)
|
||||
}
|
||||
}
|
||||
is ParsedCommand.CreateSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val params = CreateSpaceParams().apply {
|
||||
name = slashCommandResult.name
|
||||
invitedUserIds.addAll(slashCommandResult.invitees)
|
||||
}
|
||||
val spaceId = session.spaceService().createSpace(params)
|
||||
session.spaceService().getSpace(spaceId)
|
||||
?.addChildren(
|
||||
state.roomId,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.AddToSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.spaceService().getSpace(slashCommandResult.spaceId)
|
||||
?.addChildren(
|
||||
room.roomId,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.JoinSpace -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.LeaveRoom -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
session.getRoom(slashCommandResult.roomId)?.leave(null)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
||||
}
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.UpgradeRoom -> {
|
||||
_viewEvents.post(
|
||||
TextComposerViewEvents.ShowRoomUpgradeDialog(
|
||||
slashCommandResult.newVersion,
|
||||
room.roomSummary()?.isPublic ?: false
|
||||
)
|
||||
)
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
// TODO check if same content?
|
||||
room.getTimeLineEvent(inReplyTo)?.let {
|
||||
room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
|
||||
}
|
||||
} else {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val existingBody = messageContent?.body ?: ""
|
||||
if (existingBody != action.text) {
|
||||
room.editTextMessage(state.sendMode.timelineEvent,
|
||||
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
||||
action.text,
|
||||
action.autoMarkdown)
|
||||
} else {
|
||||
Timber.w("Same message content, do not send edition")
|
||||
}
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
||||
|
||||
// TODO check for pills?
|
||||
|
||||
// TODO Refactor this, just temporary for quotes
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(finalText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (finalText == htmlText) {
|
||||
room.sendTextMessage(finalText)
|
||||
} else {
|
||||
room.sendFormattedTextMessage(finalText, htmlText)
|
||||
}
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun popDraft() = withState {
|
||||
if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) {
|
||||
// If we were sharing, we want to get back our last value from draft
|
||||
loadDraftIfAny()
|
||||
} else {
|
||||
// Otherwise we clear the composer and remove the draft from db
|
||||
setState { copy(sendMode = SendMode.REGULAR("", false)) }
|
||||
viewModelScope.launch {
|
||||
room.deleteDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDraftIfAny() {
|
||||
val currentDraft = room.getDraft()
|
||||
setState {
|
||||
copy(
|
||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||
sendMode = when (currentDraft) {
|
||||
is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false)
|
||||
is UserDraft.QUOTE -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.QUOTE(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.REPLY -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.REPLY(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.EDIT -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.EDIT(timelineEvent, currentDraft.text)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: SendMode.REGULAR("", fromSharing = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserIsTyping(action: TextComposerAction.UserIsTyping) {
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
if (action.isTyping) {
|
||||
room.userIsTyping()
|
||||
} else {
|
||||
room.userStopsTyping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) {
|
||||
// If message is blank, convert to an emote, with default message
|
||||
if (sendChatEffect.message.isBlank()) {
|
||||
val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) {
|
||||
ChatEffect.CONFETTI -> R.string.default_message_emote_confetti
|
||||
ChatEffect.SNOWFALL -> R.string.default_message_emote_snow
|
||||
})
|
||||
room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE)
|
||||
} else {
|
||||
room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
session.joinRoom(command.roomAlias, command.reason, emptyList())
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
|
||||
return@launch
|
||||
}
|
||||
session.getRoomSummary(command.roomAlias)
|
||||
?.roomId
|
||||
?.let {
|
||||
_viewEvents.post(TextComposerViewEvents.JoinRoomCommandSuccess(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||
return buildString {
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].isNotBlank()) {
|
||||
append("> ")
|
||||
append(messageParagraphs[i])
|
||||
}
|
||||
|
||||
if (i != messageParagraphs.lastIndex) {
|
||||
append("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
append("\n\n")
|
||||
append(myText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.updateTopic(changeTopic.topic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.invite(invite.userId, invite.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.invite3pid(invite.threePid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
|
||||
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
||||
?.content
|
||||
?.toModel<PowerLevelsContent>()
|
||||
?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
|
||||
?.toContent()
|
||||
?: return
|
||||
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
session.setDisplayName(session.myUserId, changeDisplayName.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.kick(kick.userId, kick.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.ban(ban.userId, ban.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
|
||||
launchSlashCommandFlowSuspendable {
|
||||
room.unban(unban.userId, unban.reason)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a send mode to a draft and save the draft
|
||||
*/
|
||||
private fun handleSaveDraft(action: TextComposerAction.SaveDraft) = withState {
|
||||
session.coroutineScope.launch {
|
||||
when {
|
||||
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
|
||||
room.saveDraft(UserDraft.REGULAR(action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.REPLY -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.QUOTE -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
it.sendMode is SendMode.EDIT -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
||||
_viewEvents.post(TextComposerViewEvents.SlashCommandHandled())
|
||||
viewModelScope.launch {
|
||||
val event = try {
|
||||
block()
|
||||
TextComposerViewEvents.SlashCommandResultOk
|
||||
} catch (failure: Exception) {
|
||||
TextComposerViewEvents.SlashCommandResultError(failure)
|
||||
}
|
||||
_viewEvents.post(event)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(initialState: TextComposerViewState): TextComposerViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<TextComposerViewModel, TextComposerViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel {
|
||||
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.textComposerViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||
import im.vector.app.features.home.room.detail.SendMode
|
||||
|
||||
data class TextComposerViewState(
|
||||
val roomId: String,
|
||||
val canSendMessage: Boolean = true,
|
||||
val isSendButtonVisible: Boolean = false,
|
||||
val isRecordingVoiceMessage: Boolean = false,
|
||||
val sendMode: SendMode = SendMode.REGULAR("", false),
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(
|
||||
roomId = args.roomId,
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in a new issue