mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-28 14:08:44 +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
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
sealed class RoomDetailAction : VectorViewModelAction {
|
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 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 SendMedia(val attachments: List<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
|
@ -52,11 +49,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
object EnterTrackingUnreadMessagesState : RoomDetailAction()
|
object EnterTrackingUnreadMessagesState : RoomDetailAction()
|
||||||
object ExitTrackingUnreadMessagesState : 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 ResendMessage(val eventId: String) : RoomDetailAction()
|
||||||
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
|
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
|
||||||
data class CancelSend(val eventId: String, val force: Boolean) : 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.keysbackup.restore.KeysBackupRestoreActivity
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.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.composer.VoiceMessageRecorderView
|
||||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
@ -235,6 +239,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private val permalinkHandler: PermalinkHandler,
|
private val permalinkHandler: PermalinkHandler,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||||
|
val textComposerViewModelFactory: TextComposerViewModel.Factory,
|
||||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
|
@ -287,6 +292,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
autoCompleterFactory.create(roomDetailArgs.roomId)
|
autoCompleterFactory.create(roomDetailArgs.roomId)
|
||||||
}
|
}
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
|
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||||
private val debouncer = Debouncer(createUIHandler())
|
private val debouncer = Debouncer(createUIHandler())
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
|
@ -379,7 +385,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
updateJumpToReadMarkerViewVisibility()
|
updateJumpToReadMarkerViewVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend ->
|
textComposerViewModel.selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend ->
|
||||||
if (!canSend) {
|
if (!canSend) {
|
||||||
return@selectSubscribe
|
return@selectSubscribe
|
||||||
}
|
}
|
||||||
|
@ -391,6 +397,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
roomDetailViewModel.selectSubscribe(
|
roomDetailViewModel.selectSubscribe(
|
||||||
RoomDetailViewState::syncState,
|
RoomDetailViewState::syncState,
|
||||||
RoomDetailViewState::incrementalSyncStatus,
|
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 {
|
roomDetailViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is RoomDetailViewEvents.Failure -> {
|
is RoomDetailViewEvents.Failure -> {
|
||||||
|
@ -418,8 +434,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message)
|
||||||
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
|
||||||
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
|
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
|
||||||
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
|
||||||
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
|
||||||
is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
|
is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
|
||||||
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
|
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
|
||||||
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
|
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
|
||||||
|
@ -444,7 +458,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
||||||
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
||||||
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
||||||
is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,7 +508,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
|
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) {
|
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) {
|
||||||
val tag = MigrateRoomBottomSheet::javaClass.name
|
val tag = MigrateRoomBottomSheet::javaClass.name
|
||||||
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
|
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
|
||||||
.show(parentFragmentManager, tag)
|
.show(parentFragmentManager, tag)
|
||||||
|
@ -753,7 +766,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) {
|
private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) {
|
||||||
updateComposerText("")
|
updateComposerText("")
|
||||||
lockSendButton = false
|
lockSendButton = false
|
||||||
navigator.openRoom(vectorBaseActivity, action.roomId)
|
navigator.openRoom(vectorBaseActivity, action.roomId)
|
||||||
|
@ -762,7 +775,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private fun handleShareData() {
|
private fun handleShareData() {
|
||||||
when (val sharedData = roomDetailArgs.sharedData) {
|
when (val sharedData = roomDetailArgs.sharedData) {
|
||||||
is SharedData.Text -> {
|
is SharedData.Text -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
|
||||||
}
|
}
|
||||||
is SharedData.Attachments -> {
|
is SharedData.Attachments -> {
|
||||||
// open share edition
|
// open share edition
|
||||||
|
@ -980,9 +993,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private fun renderRegularMode(text: String) {
|
private fun renderRegularMode(text: String) {
|
||||||
autoCompleter.exitSpecialMode()
|
autoCompleter.exitSpecialMode()
|
||||||
views.composerLayout.collapse()
|
views.composerLayout.collapse()
|
||||||
|
|
||||||
views.voiceMessageRecorderView.isVisible = text.isBlank()
|
views.voiceMessageRecorderView.isVisible = text.isBlank()
|
||||||
|
|
||||||
updateComposerText(text)
|
updateComposerText(text)
|
||||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
||||||
}
|
}
|
||||||
|
@ -1077,7 +1088,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
notificationDrawerManager.setCurrentRoom(null)
|
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.
|
// 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)
|
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions)
|
||||||
|
@ -1196,12 +1207,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
||||||
val eventId = it.eventId
|
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 {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
val canSendMessage = withState(roomDetailViewModel) {
|
val canSendMessage = withState(textComposerViewModel) {
|
||||||
it.canSendMessage
|
it.canSendMessage
|
||||||
}
|
}
|
||||||
if (!canSendMessage) {
|
if (!canSendMessage) {
|
||||||
|
@ -1303,7 +1314,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseRelatedMessage() {
|
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 {
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
|
@ -1320,6 +1331,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
views.voiceMessageRecorderView.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1332,7 +1344,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// We collapse ASAP, if not there will be a slight annoying delay
|
// We collapse ASAP, if not there will be a slight annoying delay
|
||||||
views.composerLayout.collapse(true)
|
views.composerLayout.collapse(true)
|
||||||
lockSendButton = true
|
lockSendButton = true
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||||
emojiPopup.dismiss()
|
emojiPopup.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1344,7 +1356,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
.subscribe {
|
.subscribe {
|
||||||
Timber.d("Typing: User is typing: $it")
|
Timber.d("Typing: User is typing: $it")
|
||||||
roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it))
|
textComposerViewModel.handle(TextComposerAction.UserIsTyping(it))
|
||||||
}
|
}
|
||||||
.disposeOnDestroyView()
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
@ -1364,24 +1376,24 @@ class RoomDetailFragment @Inject constructor(
|
||||||
return isHandled
|
return isHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(roomDetailViewModel) { state ->
|
override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState ->
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
val summary = state.asyncRoomSummary()
|
val summary = mainState.asyncRoomSummary()
|
||||||
renderToolbar(summary, state.typingMessage)
|
renderToolbar(summary, mainState.formattedTypingUsers)
|
||||||
views.removeJitsiWidgetView.render(state)
|
views.removeJitsiWidgetView.render(mainState)
|
||||||
if (state.hasFailedSending) {
|
if (mainState.hasFailedSending) {
|
||||||
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true
|
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false
|
lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false
|
||||||
}
|
}
|
||||||
val inviter = state.asyncInviter()
|
val inviter = mainState.asyncInviter()
|
||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
views.jumpToBottomView.count = summary.notificationCount
|
views.jumpToBottomView.count = summary.notificationCount
|
||||||
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
|
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
|
||||||
timelineEventController.update(state)
|
timelineEventController.update(mainState)
|
||||||
lazyLoadedViews.inviteView(false)?.isVisible = false
|
lazyLoadedViews.inviteView(false)?.isVisible = false
|
||||||
if (state.tombstoneEvent == null) {
|
if (mainState.tombstoneEvent == null) {
|
||||||
if (state.canSendMessage) {
|
if (textComposerState.canSendMessage) {
|
||||||
if (!views.voiceMessageRecorderView.isActive()) {
|
if (!views.voiceMessageRecorderView.isActive()) {
|
||||||
views.composerLayout.isVisible = true
|
views.composerLayout.isVisible = true
|
||||||
views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse()
|
views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse()
|
||||||
|
@ -1390,30 +1402,32 @@ class RoomDetailFragment @Inject constructor(
|
||||||
views.composerLayout.alwaysShowSendButton = false
|
views.composerLayout.alwaysShowSendButton = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
views.composerLayout.isVisible = false
|
views.hideComposerViews()
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
|
||||||
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
views.composerLayout.isVisible = false
|
views.hideComposerViews()
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent))
|
||||||
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
|
|
||||||
}
|
}
|
||||||
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||||
views.composerLayout.isVisible = false
|
views.hideComposerViews()
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
|
||||||
lazyLoadedViews.inviteView(true)?.apply {
|
lazyLoadedViews.inviteView(true)?.apply {
|
||||||
callback = this@RoomDetailFragment
|
callback = this@RoomDetailFragment
|
||||||
isVisible = true
|
isVisible = true
|
||||||
render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
|
render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState)
|
||||||
setOnClickListener { }
|
setOnClickListener { }
|
||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
} else if (state.asyncInviter.complete) {
|
} else if (mainState.asyncInviter.complete) {
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun FragmentRoomDetailBinding.hideComposerViews() {
|
||||||
|
composerLayout.isVisible = false
|
||||||
|
voiceMessageRecorderView.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
|
private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
|
||||||
if (roomSummary == null) {
|
if (roomSummary == null) {
|
||||||
views.roomToolbarContentView.isClickable = false
|
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) {
|
when (sendMessageResult) {
|
||||||
is RoomDetailViewEvents.SlashCommandHandled -> {
|
is TextComposerViewEvents.SlashCommandHandled -> {
|
||||||
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandError -> {
|
is TextComposerViewEvents.SlashCommandError -> {
|
||||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
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))
|
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandResultOk -> {
|
is TextComposerViewEvents.SlashCommandResultOk -> {
|
||||||
updateComposerText("")
|
updateComposerText("")
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandResultError -> {
|
is TextComposerViewEvents.SlashCommandResultError -> {
|
||||||
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
|
||||||
}
|
}
|
||||||
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
|
is TextComposerViewEvents.SlashCommandNotImplemented -> {
|
||||||
displayCommandError(getString(R.string.not_implemented))
|
displayCommandError(getString(R.string.not_implemented))
|
||||||
}
|
}
|
||||||
} // .exhaustive
|
} // .exhaustive
|
||||||
|
@ -1938,17 +1952,17 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
is EventSharedAction.Edit -> {
|
is EventSharedAction.Edit -> {
|
||||||
if (!views.voiceMessageRecorderView.isActive()) {
|
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 {
|
} else {
|
||||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is EventSharedAction.Quote -> {
|
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 -> {
|
is EventSharedAction.Reply -> {
|
||||||
if (!views.voiceMessageRecorderView.isActive()) {
|
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 {
|
} else {
|
||||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||||
}
|
}
|
||||||
|
@ -2160,7 +2174,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
super.onContactAttachmentReady(contactAttachment)
|
super.onContactAttachmentReady(contactAttachment)
|
||||||
val formattedContact = contactAttachment.toHumanReadable()
|
val formattedContact = contactAttachment.toHumanReadable()
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
|
textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onViewWidgetsClicked() {
|
private fun onViewWidgetsClicked() {
|
||||||
|
|
|
@ -66,8 +66,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||||
val mimeType: String?
|
val mimeType: String?
|
||||||
) : RoomDetailViewEvents()
|
) : RoomDetailViewEvents()
|
||||||
|
|
||||||
abstract class SendMessageResult : RoomDetailViewEvents()
|
|
||||||
|
|
||||||
data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents()
|
data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents()
|
||||||
|
|
||||||
object DisplayPromptForIntegrationManager : RoomDetailViewEvents()
|
object DisplayPromptForIntegrationManager : RoomDetailViewEvents()
|
||||||
|
@ -82,19 +80,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||||
val domain: String,
|
val domain: String,
|
||||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents()
|
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()
|
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||||
object StopChatEffects : RoomDetailViewEvents()
|
object StopChatEffects : RoomDetailViewEvents()
|
||||||
object RoomReplacementStarted : 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.conference.JitsiService
|
||||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
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.createdirect.DirectRoomHelper
|
||||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
|
@ -67,9 +65,6 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
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.ChangeMembershipState
|
||||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
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.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.RoomMemberSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
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.message.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
|
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.powerlevels.PowerLevelsHelper
|
||||||
import org.matrix.android.sdk.api.session.room.read.ReadService
|
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.Timeline
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
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.session.widgets.model.WidgetType
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||||
|
@ -181,7 +168,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
observeSyncState()
|
observeSyncState()
|
||||||
observeDataStore()
|
observeDataStore()
|
||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
loadDraftIfAny()
|
|
||||||
observeUnreadState()
|
observeUnreadState()
|
||||||
observeMyRoomMember()
|
observeMyRoomMember()
|
||||||
observeActiveRoomWidgets()
|
observeActiveRoomWidgets()
|
||||||
|
@ -235,13 +221,11 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
private fun observePowerLevel() {
|
private fun observePowerLevel() {
|
||||||
PowerLevelsObservableFactory(room).createObservable()
|
PowerLevelsObservableFactory(room).createObservable()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
|
|
||||||
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
|
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
|
||||||
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
|
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
|
||||||
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
|
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
canSendMessage = canSendMessage,
|
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
isAllowedToManageWidgets = isAllowedToManageWidgets,
|
isAllowedToManageWidgets = isAllowedToManageWidgets,
|
||||||
isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall
|
isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall
|
||||||
|
@ -300,10 +284,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun handle(action: RoomDetailAction) {
|
override fun handle(action: RoomDetailAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
|
||||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
|
||||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
|
||||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||||
|
@ -315,10 +296,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(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.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||||
is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom()
|
is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom()
|
||||||
|
@ -590,70 +567,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
return room.getRoomMember(userId)
|
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) {
|
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
|
||||||
// Ensure outbound session keys
|
// Ensure outbound session keys
|
||||||
if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
|
if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
|
||||||
|
@ -768,416 +681,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// 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) {
|
private fun handleSendReaction(action: RoomDetailAction.SendReaction) {
|
||||||
room.sendReaction(action.targetEventId, action.reaction)
|
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) {
|
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
|
||||||
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||||
val isLocalSendingFile = action.senderId == session.myUserId
|
val isLocalSendingFile = action.senderId == session.myUserId
|
||||||
|
@ -1604,7 +1085,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
setState {
|
setState {
|
||||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||||
copy(
|
copy(
|
||||||
typingMessage = typingMessage,
|
formattedTypingUsers = typingMessage,
|
||||||
hasFailedSending = summary.hasFailedSending
|
hasFailedSending = summary.hasFailedSending
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ data class RoomDetailViewState(
|
||||||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
|
||||||
val typingMessage: String? = null,
|
val formattedTypingUsers: String? = null,
|
||||||
val sendMode: SendMode = SendMode.REGULAR("", false),
|
val sendMode: SendMode = SendMode.REGULAR("", false),
|
||||||
val tombstoneEvent: Event? = null,
|
val tombstoneEvent: Event? = null,
|
||||||
val joinUpgradedRoomAsync: Async<String> = Uninitialized,
|
val joinUpgradedRoomAsync: Async<String> = Uninitialized,
|
||||||
|
@ -84,7 +84,6 @@ data class RoomDetailViewState(
|
||||||
val unreadState: UnreadState = UnreadState.Unknown,
|
val unreadState: UnreadState = UnreadState.Unknown,
|
||||||
val canShowJumpToReadMarker: Boolean = true,
|
val canShowJumpToReadMarker: Boolean = true,
|
||||||
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
|
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
|
||||||
val canSendMessage: Boolean = true,
|
|
||||||
val canInvite: Boolean = true,
|
val canInvite: Boolean = true,
|
||||||
val isAllowedToManageWidgets: Boolean = false,
|
val isAllowedToManageWidgets: Boolean = false,
|
||||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
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