Merge pull request #4523 from vector-im/feature/adm/voice-composer

Moving voice logic to the MessageComposer
This commit is contained in:
Benoit Marty 2021-11-22 15:01:40 +01:00 committed by GitHub
commit 97a44a5632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 249 additions and 242 deletions

View File

@ -41,7 +41,7 @@ import im.vector.app.features.home.PromoteRestrictedViewModel
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.UnreadMessagesSharedViewModel
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel import im.vector.app.features.home.room.detail.RoomDetailViewModel
import im.vector.app.features.home.room.detail.search.SearchViewModel import im.vector.app.features.home.room.detail.search.SearchViewModel
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -505,8 +505,8 @@ interface MavericksViewModelModule {
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(TextComposerViewModel::class) @MavericksViewModelKey(RoomDetailViewModel::class)
fun textComposerViewModelFactory(factory: TextComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap

View File

@ -21,7 +21,6 @@ import android.view.View
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.ConferenceEvent
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -108,12 +107,4 @@ sealed class RoomDetailAction : VectorViewModelAction {
object RemoveAllFailedMessages : RoomDetailAction() object RemoveAllFailedMessages : RoomDetailAction()
data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction() data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction()
// Voice Message
object StartRecordingVoiceMessage : RoomDetailAction()
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : RoomDetailAction()
object PauseRecordingVoiceMessage : RoomDetailAction()
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction()
object PlayOrPauseRecordingPlayback : RoomDetailAction()
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : RoomDetailAction()
} }

View File

@ -131,12 +131,12 @@ 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.MessageComposerAction
import im.vector.app.features.home.room.detail.composer.MessageComposerView
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewState
import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.SendMode
import im.vector.app.features.home.room.detail.composer.TextComposerAction
import im.vector.app.features.home.room.detail.composer.TextComposerView
import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
import im.vector.app.features.home.room.detail.composer.TextComposerViewState
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
@ -240,7 +240,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleterFactory: AutoCompleter.Factory, autoCompleterFactory: AutoCompleter.Factory,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val messageComposerViewModelFactory: MessageComposerViewModel.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,
@ -293,7 +293,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 messageComposerViewModel: MessageComposerViewModel by fragmentViewModel()
private val debouncer = Debouncer(createUIHandler()) private val debouncer = Debouncer(createUIHandler())
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@ -386,7 +386,7 @@ class RoomDetailFragment @Inject constructor(
updateJumpToReadMarkerViewVisibility() updateJumpToReadMarkerViewVisibility()
} }
textComposerViewModel.onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
if (!canSend) { if (!canSend) {
return@onEach return@onEach
} }
@ -411,25 +411,26 @@ class RoomDetailFragment @Inject constructor(
) )
} }
textComposerViewModel.observeViewEvents { messageComposerViewModel.observeViewEvents {
when (it) { when (it) {
is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it) is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
is TextComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId) is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
}.exhaustive is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> {
if (it.throwable is VoiceFailure.UnableToRecord) { if (it.throwable is VoiceFailure.UnableToRecord) {
onCannotRecord() onCannotRecord()
} }
showErrorInSnackbar(it.throwable) showErrorInSnackbar(it.throwable)
} }
}.exhaustive
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
@ -469,7 +470,7 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun handleSendButtonVisibilityChanged(event: TextComposerViewEvents.AnimateSendButtonVisibility) { private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
if (event.isVisible) { if (event.isVisible) {
views.voiceMessageRecorderView.isVisible = false views.voiceMessageRecorderView.isVisible = false
views.composerLayout.views.sendButton.alpha = 0f views.composerLayout.views.sendButton.alpha = 0f
@ -505,7 +506,7 @@ class RoomDetailFragment @Inject constructor(
private fun onCannotRecord() { private fun onCannotRecord() {
// Update the UI, cancel the animation // Update the UI, cancel the animation
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None)) messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
} }
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
@ -524,7 +525,7 @@ class RoomDetailFragment @Inject constructor(
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag) JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
} }
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) { private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: MessageComposerViewEvents.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)
@ -697,18 +698,18 @@ class RoomDetailFragment @Inject constructor(
override fun onVoiceRecordingStarted() { override fun onVoiceRecordingStarted() {
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
vibrate(requireContext()) vibrate(requireContext())
updateRecordingUiState(RecordingUiState.Started) updateRecordingUiState(RecordingUiState.Started)
} }
} }
override fun onVoicePlaybackButtonClicked() { override fun onVoicePlaybackButtonClicked() {
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback) messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseRecordingPlayback)
} }
override fun onVoiceRecordingCancelled() { override fun onVoiceRecordingCancelled() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true)) messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled) updateRecordingUiState(RecordingUiState.Cancelled)
} }
@ -721,27 +722,27 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onSendVoiceMessage() { override fun onSendVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false)) messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None) updateRecordingUiState(RecordingUiState.None)
} }
override fun onDeleteVoiceMessage() { override fun onDeleteVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true)) messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.None) updateRecordingUiState(RecordingUiState.None)
} }
override fun onRecordingLimitReached() { override fun onRecordingLimitReached() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage) messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback) updateRecordingUiState(RecordingUiState.Playback)
} }
override fun onRecordingWaveformClicked() { override fun onRecordingWaveformClicked() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage) messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback) updateRecordingUiState(RecordingUiState.Playback)
} }
private fun updateRecordingUiState(state: RecordingUiState) { private fun updateRecordingUiState(state: RecordingUiState) {
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(state)) messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
} }
} }
} }
@ -818,7 +819,7 @@ class RoomDetailFragment @Inject constructor(
.show() .show()
} }
private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) { private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
views.composerLayout.setTextIfDifferent("") views.composerLayout.setTextIfDifferent("")
lockSendButton = false lockSendButton = false
navigator.openRoom(vectorBaseActivity, action.roomId) navigator.openRoom(vectorBaseActivity, action.roomId)
@ -827,7 +828,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 -> {
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true)) messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
} }
is SharedData.Attachments -> { is SharedData.Attachments -> {
// open share edition // open share edition
@ -1132,11 +1133,11 @@ class RoomDetailFragment @Inject constructor(
notificationDrawerManager.setCurrentRoom(null) notificationDrawerManager.setCurrentRoom(null)
textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.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(deleteRecord = false)) messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
views.voiceMessageRecorderView.display(RecordingUiState.None) views.voiceMessageRecorderView.render(RecordingUiState.None)
} }
private val attachmentFileActivityResultLauncher = registerStartForActivityResult { private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
@ -1251,12 +1252,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
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString()))
} }
} }
override fun canSwipeModel(model: EpoxyModel<*>): Boolean { override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
val canSendMessage = withState(textComposerViewModel) { val canSendMessage = withState(messageComposerViewModel) {
it.canSendMessage it.canSendMessage
} }
if (!canSendMessage) { if (!canSendMessage) {
@ -1345,7 +1346,7 @@ class RoomDetailFragment @Inject constructor(
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard() views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
views.composerLayout.callback = object : TextComposerView.Callback { views.composerLayout.callback = object : MessageComposerView.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) { if (!::attachmentTypeSelector.isInitialized) {
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment) attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
@ -1358,7 +1359,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onCloseRelatedMessage() { override fun onCloseRelatedMessage() {
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false)) messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false))
} }
override fun onRichContentSelected(contentUri: Uri): Boolean { override fun onRichContentSelected(contentUri: Uri): Boolean {
@ -1366,7 +1367,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onTextChanged(text: CharSequence) { override fun onTextChanged(text: CharSequence) {
textComposerViewModel.handle(TextComposerAction.OnTextChanged(text)) messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
} }
} }
} }
@ -1380,7 +1381,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
textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
emojiPopup.dismiss() emojiPopup.dismiss()
} }
} }
@ -1392,7 +1393,7 @@ class RoomDetailFragment @Inject constructor(
.map { it.isNotEmpty() } .map { it.isNotEmpty() }
.onEach { .onEach {
Timber.d("Typing: User is typing: $it") Timber.d("Typing: User is typing: $it")
textComposerViewModel.handle(TextComposerAction.UserIsTyping(it)) messageComposerViewModel.handle(MessageComposerAction.UserIsTyping(it))
} }
.launchIn(viewLifecycleOwner.lifecycleScope) .launchIn(viewLifecycleOwner.lifecycleScope)
@ -1412,7 +1413,7 @@ class RoomDetailFragment @Inject constructor(
return isHandled return isHandled
} }
override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState -> override fun invalidate() = withState(roomDetailViewModel, messageComposerViewModel) { mainState, messageComposerState ->
invalidateOptionsMenu() invalidateOptionsMenu()
val summary = mainState.asyncRoomSummary() val summary = mainState.asyncRoomSummary()
renderToolbar(summary, mainState.formattedTypingUsers) renderToolbar(summary, mainState.formattedTypingUsers)
@ -1429,13 +1430,13 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.update(mainState) timelineEventController.update(mainState)
lazyLoadedViews.inviteView(false)?.isVisible = false lazyLoadedViews.inviteView(false)?.isVisible = false
if (mainState.tombstoneEvent == null) { if (mainState.tombstoneEvent == null) {
views.composerLayout.isInvisible = !textComposerState.isComposerVisible views.composerLayout.isInvisible = !messageComposerState.isComposerVisible
views.voiceMessageRecorderView.isVisible = textComposerState.isVoiceMessageRecorderVisible views.voiceMessageRecorderView.isVisible = messageComposerState.isVoiceMessageRecorderVisible
views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible views.composerLayout.views.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
views.voiceMessageRecorderView.display(textComposerState.voiceRecordingUiState) views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState)
views.composerLayout.setRoomEncrypted(summary.isEncrypted) views.composerLayout.setRoomEncrypted(summary.isEncrypted)
// views.composerLayout.alwaysShowSendButton = false // views.composerLayout.alwaysShowSendButton = false
if (textComposerState.canSendMessage) { if (messageComposerState.canSendMessage) {
views.notificationAreaView.render(NotificationAreaView.State.Hidden) views.notificationAreaView.render(NotificationAreaView.State.Hidden)
} else { } else {
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
@ -1492,27 +1493,27 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: MessageComposerViewEvents.SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is TextComposerViewEvents.SlashCommandLoading -> { is MessageComposerViewEvents.SlashCommandLoading -> {
showLoading(null) showLoading(null)
} }
is TextComposerViewEvents.SlashCommandError -> { is MessageComposerViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
} }
is TextComposerViewEvents.SlashCommandUnknown -> { is MessageComposerViewEvents.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
} }
is TextComposerViewEvents.SlashCommandResultOk -> { is MessageComposerViewEvents.SlashCommandResultOk -> {
dismissLoadingDialog() dismissLoadingDialog()
views.composerLayout.setTextIfDifferent("") views.composerLayout.setTextIfDifferent("")
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
} }
is TextComposerViewEvents.SlashCommandResultError -> { is MessageComposerViewEvents.SlashCommandResultError -> {
dismissLoadingDialog() dismissLoadingDialog()
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
} }
is TextComposerViewEvents.SlashCommandNotImplemented -> { is MessageComposerViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented)) displayCommandError(getString(R.string.not_implemented))
} }
} // .exhaustive } // .exhaustive
@ -1883,7 +1884,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) { override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) {
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent)) messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
} }
private fun onShareActionClicked(action: EventSharedAction.Share) { private fun onShareActionClicked(action: EventSharedAction.Share) {
@ -1988,18 +1989,18 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
} }
is EventSharedAction.Edit -> { is EventSharedAction.Edit -> {
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.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 -> {
textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
} }
is EventSharedAction.Reply -> { is EventSharedAction.Reply -> {
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.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)
} }
@ -2212,7 +2213,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()
textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false)) messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
} }
private fun onViewWidgetsClicked() { private fun onViewWidgetsClicked() {

View File

@ -21,24 +21,23 @@ import androidx.annotation.IdRes
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.flow.chunk import im.vector.app.core.flow.chunk
import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.ConferenceEvent
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.conference.JitsiService
@ -47,7 +46,6 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
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
import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
@ -56,7 +54,6 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoicePlayerHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -116,8 +113,6 @@ class RoomDetailViewModel @AssistedInject constructor(
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService, private val jitsiService: JitsiService,
private val activeConferenceHolder: JitsiActiveConferenceHolder, private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper,
timelineFactory: TimelineFactory timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -144,22 +139,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private var prepareToEncrypt: Async<Unit> = Uninitialized private var prepareToEncrypt: Async<Unit> = Uninitialized
@AssistedFactory @AssistedFactory
interface Factory { interface Factory : MavericksAssistedViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
fun create(initialState: RoomDetailViewState): RoomDetailViewModel override fun create(initialState: RoomDetailViewState): RoomDetailViewModel
} }
/** companion object : MavericksViewModelFactory<RoomDetailViewModel, RoomDetailViewState> by hiltMavericksViewModelFactory() {
* Can't use the hiltMaverick here because some dependencies are injected here and in fragment but they don't share the graph.
*/
companion object : MavericksViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
const val PAGINATION_COUNT = 50 const val PAGINATION_COUNT = 50
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel {
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.roomDetailViewModelFactory.create(state)
}
} }
init { init {
@ -343,12 +328,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
RoomDetailAction.ResendAll -> handleResendAll() RoomDetailAction.ResendAll -> handleResendAll()
RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
is RoomDetailAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
RoomDetailAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
RoomDetailAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is RoomDetailAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
is RoomDetailAction.RoomUpgradeSuccess -> { is RoomDetailAction.RoomUpgradeSuccess -> {
setState { setState {
copy(joinUpgradedRoomAsync = Success(action.replacementRoomId)) copy(joinUpgradedRoomAsync = Success(action.replacementRoomId))
@ -612,56 +591,6 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleStartRecordingVoiceMessage() {
try {
voiceMessageHelper.startRecording()
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
}
}
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
voiceMessageHelper.stopPlayback()
if (isCancelled) {
voiceMessageHelper.deleteRecording()
} else {
voiceMessageHelper.stopRecording()?.let { audioType ->
if (audioType.duration > 1000) {
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
} else {
voiceMessageHelper.deleteRecording()
}
}
}
}
private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) {
viewModelScope.launch(Dispatchers.IO) {
try {
// Download can fail
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
// Conversion can fail, fallback to the original file in this case and let the player fail for us
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
// Play can fail
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
}
}
}
private fun handlePlayOrPauseRecordingPlayback() {
voiceMessageHelper.startOrPauseRecordingPlayback()
}
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
}
private fun handlePauseRecordingVoiceMessage() {
voiceMessageHelper.pauseRecording()
}
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->

View File

@ -18,15 +18,24 @@ package im.vector.app.features.home.room.detail.composer
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
sealed class TextComposerAction : VectorViewModelAction { sealed class MessageComposerAction : VectorViewModelAction {
data class SaveDraft(val draft: String) : TextComposerAction() data class SaveDraft(val draft: String) : MessageComposerAction()
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : TextComposerAction() data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
data class EnterEditMode(val eventId: String, val text: String) : TextComposerAction() data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterQuoteMode(val eventId: String, val text: String) : TextComposerAction() data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction() data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction() data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
data class OnTextChanged(val text: CharSequence) : TextComposerAction() data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : TextComposerAction()
// Voice Message
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
object StartRecordingVoiceMessage : MessageComposerAction()
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
object PauseRecordingVoiceMessage : MessageComposerAction()
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
object PlayOrPauseRecordingPlayback : MessageComposerAction()
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
} }

View File

@ -36,7 +36,7 @@ import im.vector.app.databinding.ComposerLayoutBinding
/** /**
* Encapsulate the timeline composer UX. * Encapsulate the timeline composer UX.
*/ */
class TextComposerView @JvmOverloads constructor( class MessageComposerView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {

View File

@ -20,13 +20,13 @@ import androidx.annotation.StringRes
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
sealed class TextComposerViewEvents : VectorViewEvents { sealed class MessageComposerViewEvents : VectorViewEvents {
data class AnimateSendButtonVisibility(val isVisible: Boolean) : TextComposerViewEvents() data class AnimateSendButtonVisibility(val isVisible: Boolean) : MessageComposerViewEvents()
data class ShowMessage(val message: String) : TextComposerViewEvents() data class ShowMessage(val message: String) : MessageComposerViewEvents()
abstract class SendMessageResult : TextComposerViewEvents() abstract class SendMessageResult : MessageComposerViewEvents()
object MessageSent : SendMessageResult() object MessageSent : SendMessageResult()
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
@ -36,10 +36,12 @@ sealed class TextComposerViewEvents : VectorViewEvents {
data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult() data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
data class OpenRoomMemberProfile(val userId: String) : TextComposerViewEvents() data class OpenRoomMemberProfile(val userId: String) : MessageComposerViewEvents()
// TODO Remove // TODO Remove
object SlashCommandNotImplemented : SendMessageResult() object SlashCommandNotImplemented : SendMessageResult()
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents() data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : MessageComposerViewEvents()
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : MessageComposerViewEvents()
} }

View File

@ -16,24 +16,27 @@
package im.vector.app.features.home.room.detail.composer package im.vector.app.features.home.room.detail.composer
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
import im.vector.app.features.command.ParsedCommand import im.vector.app.features.command.ParsedCommand
import im.vector.app.features.home.room.detail.ChatEffect import im.vector.app.features.home.room.detail.ChatEffect
import im.vector.app.features.home.room.detail.RoomDetailFragment
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator 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.home.room.detail.toMessageType
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoicePlayerHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
@ -55,13 +58,15 @@ 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.space.CreateSpaceParams
import timber.log.Timber import timber.log.Timber
class TextComposerViewModel @AssistedInject constructor( class MessageComposerViewModel @AssistedInject constructor(
@Assisted initialState: TextComposerViewState, @Assisted initialState: MessageComposerViewState,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val rainbowGenerator: RainbowGenerator private val rainbowGenerator: RainbowGenerator,
) : VectorViewModel<TextComposerViewState, TextComposerAction, TextComposerViewEvents>(initialState) { private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
@ -74,26 +79,32 @@ class TextComposerViewModel @AssistedInject constructor(
subscribeToStateInternal() subscribeToStateInternal()
} }
override fun handle(action: TextComposerAction) { override fun handle(action: MessageComposerAction) {
Timber.v("Handle action: $action") Timber.v("Handle action: $action")
when (action) { when (action) {
is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
is TextComposerAction.SaveDraft -> handleSaveDraft(action) is MessageComposerAction.SaveDraft -> handleSaveDraft(action)
is TextComposerAction.SendMessage -> handleSendMessage(action) is MessageComposerAction.SendMessage -> handleSendMessage(action)
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
is TextComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
} }
} }
private fun handleOnVoiceRecordingUiStateChanged(action: TextComposerAction.OnVoiceRecordingUiStateChanged) = setState { private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState {
copy(voiceRecordingUiState = action.uiState) copy(voiceRecordingUiState = action.uiState)
} }
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
setState { setState {
// Makes sure currentComposerText is upToDate when accessing further setState // Makes sure currentComposerText is upToDate when accessing further setState
currentComposerText = action.text currentComposerText = action.text
@ -103,7 +114,7 @@ class TextComposerViewModel @AssistedInject constructor(
} }
private fun subscribeToStateInternal() { private fun subscribeToStateInternal() {
onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage, MessageComposerViewState::isVoiceRecording) { _, _, _ ->
updateIsSendButtonVisibility(false) updateIsSendButtonVisibility(false)
} }
} }
@ -111,16 +122,16 @@ class TextComposerViewModel @AssistedInject constructor(
private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState { private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState {
val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank()) val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank())
if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) { if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) {
_viewEvents.post(TextComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible)) _viewEvents.post(MessageComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible))
} }
copy(isSendButtonVisible = isSendButtonVisible) copy(isSendButtonVisible = isSendButtonVisible)
} }
private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState { private fun handleEnterRegularMode(action: MessageComposerAction.EnterRegularMode) = setState {
copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
} }
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) { private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) } setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) }
} }
@ -134,19 +145,19 @@ class TextComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleEnterQuoteMode(action: TextComposerAction.EnterQuoteMode) { private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
} }
} }
private fun handleEnterReplyMode(action: TextComposerAction.EnterReplyMode) { private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
} }
} }
private fun handleSendMessage(action: TextComposerAction.SendMessage) { private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
withState { state -> withState { state ->
when (state.sendMode) { when (state.sendMode) {
is SendMode.REGULAR -> { is SendMode.REGULAR -> {
@ -154,22 +165,22 @@ class TextComposerViewModel @AssistedInject constructor(
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command)) _viewEvents.post(MessageComposerViewEvents.SlashCommandError(slashCommandResult.command))
} }
is ParsedCommand.ErrorEmptySlashCommand -> { is ParsedCommand.ErrorEmptySlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/")) _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown("/"))
} }
is ParsedCommand.ErrorUnknownSlashCommand -> { is ParsedCommand.ErrorUnknownSlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
} }
is ParsedCommand.SendPlainText -> { is ParsedCommand.SendPlainText -> {
// Send the text message to the room, without markdown // Send the text message to the room, without markdown
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
_viewEvents.post(TextComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
} }
is ParsedCommand.ChangeRoomName -> { is ParsedCommand.ChangeRoomName -> {
@ -186,11 +197,11 @@ class TextComposerViewModel @AssistedInject constructor(
} }
is ParsedCommand.ClearScalarToken -> { is ParsedCommand.ClearScalarToken -> {
// TODO // TODO
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) _viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.SetMarkdown -> { is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk( _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
popDraft() popDraft()
} }
@ -218,21 +229,21 @@ class TextComposerViewModel @AssistedInject constructor(
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendRainbow -> { is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
} }
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendRainbowEmote -> { is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
} }
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendSpoiler -> { is ParsedCommand.SendSpoiler -> {
@ -240,22 +251,22 @@ class TextComposerViewModel @AssistedInject constructor(
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>" "<span data-mx-spoiler>${slashCommandResult.message}</span>"
) )
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendShrug -> { is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message) sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendLenny -> { is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message) sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.SendChatEffect -> { is ParsedCommand.SendChatEffect -> {
sendChatEffect(slashCommandResult) sendChatEffect(slashCommandResult)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
@ -274,25 +285,25 @@ class TextComposerViewModel @AssistedInject constructor(
handleChangeAvatarForRoomSlashCommand(slashCommandResult) handleChangeAvatarForRoomSlashCommand(slashCommandResult)
} }
is ParsedCommand.ShowUser -> { is ParsedCommand.ShowUser -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
handleWhoisSlashCommand(slashCommandResult) handleWhoisSlashCommand(slashCommandResult)
popDraft() popDraft()
} }
is ParsedCommand.DiscardSession -> { is ParsedCommand.DiscardSession -> {
if (room.isEncrypted()) { if (room.isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId) session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} else { } else {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
_viewEvents.post( _viewEvents.post(
TextComposerViewEvents MessageComposerViewEvents
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
) )
} }
} }
is ParsedCommand.CreateSpace -> { is ParsedCommand.CreateSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val params = CreateSpaceParams().apply { val params = CreateSpaceParams().apply {
@ -308,15 +319,15 @@ class TextComposerViewModel @AssistedInject constructor(
true true
) )
popDraft() popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit Unit
} }
is ParsedCommand.AddToSpace -> { is ParsedCommand.AddToSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
session.spaceService().getSpace(slashCommandResult.spaceId) session.spaceService().getSpace(slashCommandResult.spaceId)
@ -327,22 +338,22 @@ class TextComposerViewModel @AssistedInject constructor(
false false
) )
popDraft() popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit Unit
} }
is ParsedCommand.JoinSpace -> { is ParsedCommand.JoinSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
popDraft() popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit Unit
@ -352,21 +363,21 @@ class TextComposerViewModel @AssistedInject constructor(
try { try {
session.getRoom(slashCommandResult.roomId)?.leave(null) session.getRoom(slashCommandResult.roomId)?.leave(null)
popDraft() popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
} }
} }
Unit Unit
} }
is ParsedCommand.UpgradeRoom -> { is ParsedCommand.UpgradeRoom -> {
_viewEvents.post( _viewEvents.post(
TextComposerViewEvents.ShowRoomUpgradeDialog( MessageComposerViewEvents.ShowRoomUpgradeDialog(
slashCommandResult.newVersion, slashCommandResult.newVersion,
room.roomSummary()?.isPublic ?: false room.roomSummary()?.isPublic ?: false
) )
) )
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft() popDraft()
} }
}.exhaustive }.exhaustive
@ -391,7 +402,7 @@ class TextComposerViewModel @AssistedInject constructor(
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
} }
_viewEvents.post(TextComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
@ -412,13 +423,13 @@ class TextComposerViewModel @AssistedInject constructor(
} else { } else {
room.sendFormattedTextMessage(finalText, htmlText) room.sendFormattedTextMessage(finalText, htmlText)
} }
_viewEvents.post(TextComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.REPLY -> { is SendMode.REPLY -> {
state.sendMode.timelineEvent.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown) room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent) _viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft() popDraft()
} }
} }
@ -467,7 +478,7 @@ class TextComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleUserIsTyping(action: TextComposerAction.UserIsTyping) { private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
if (action.isTyping) { if (action.isTyping) {
room.userIsTyping() room.userIsTyping()
@ -495,13 +506,13 @@ class TextComposerViewModel @AssistedInject constructor(
try { try {
session.joinRoom(command.roomAlias, command.reason, emptyList()) session.joinRoom(command.roomAlias, command.reason, emptyList())
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
return@launch return@launch
} }
session.getRoomSummary(command.roomAlias) session.getRoomSummary(command.roomAlias)
?.roomId ?.roomId
?.let { ?.let {
_viewEvents.post(TextComposerViewEvents.JoinRoomCommandSuccess(it)) _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it))
} }
} }
} }
@ -648,7 +659,7 @@ class TextComposerViewModel @AssistedInject constructor(
} }
private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) { private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) {
_viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId)) _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
} }
private fun sendPrefixedMessage(prefix: String, message: CharSequence) { private fun sendPrefixedMessage(prefix: String, message: CharSequence) {
@ -665,7 +676,7 @@ class TextComposerViewModel @AssistedInject constructor(
/** /**
* Convert a send mode to a draft and save the draft * Convert a send mode to a draft and save the draft
*/ */
private fun handleSaveDraft(action: TextComposerAction.SaveDraft) = withState { private fun handleSaveDraft(action: MessageComposerAction.SaveDraft) = withState {
session.coroutineScope.launch { session.coroutineScope.launch {
when { when {
it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
@ -688,24 +699,88 @@ class TextComposerViewModel @AssistedInject constructor(
} }
} }
private fun handleStartRecordingVoiceMessage() {
try {
voiceMessageHelper.startRecording()
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
}
}
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
voiceMessageHelper.stopPlayback()
if (isCancelled) {
voiceMessageHelper.deleteRecording()
} else {
voiceMessageHelper.stopRecording()?.let { audioType ->
if (audioType.duration > 1000) {
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
} else {
voiceMessageHelper.deleteRecording()
}
}
}
}
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
viewModelScope.launch(Dispatchers.IO) {
try {
// Download can fail
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
// Conversion can fail, fallback to the original file in this case and let the player fail for us
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
// Play can fail
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
}
}
}
private fun handlePlayOrPauseRecordingPlayback() {
voiceMessageHelper.startOrPauseRecordingPlayback()
}
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
}
private fun handlePauseRecordingVoiceMessage() {
voiceMessageHelper.pauseRecording()
}
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading) _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
block() block()
popDraft() popDraft()
TextComposerViewEvents.SlashCommandResultOk() MessageComposerViewEvents.SlashCommandResultOk()
} catch (failure: Throwable) { } catch (failure: Throwable) {
TextComposerViewEvents.SlashCommandResultError(failure) MessageComposerViewEvents.SlashCommandResultError(failure)
} }
_viewEvents.post(event) _viewEvents.post(event)
} }
} }
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<TextComposerViewModel, TextComposerViewState> { interface Factory {
override fun create(initialState: TextComposerViewState): TextComposerViewModel fun create(initialState: MessageComposerViewState): MessageComposerViewModel
} }
companion object : MavericksViewModelFactory<TextComposerViewModel, TextComposerViewState> by hiltMavericksViewModelFactory() /**
* We're unable to create this ViewModel with `by hiltMavericksViewModelFactory()` due to the
* VoiceMessagePlaybackTracker being ActivityScoped
*
* This factory allows us to provide the ViewModel instance from the Fragment directly
* bypassing the Singleton scope requirement
*/
companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageComposerViewState): MessageComposerViewModel {
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageComposerViewModelFactory.create(state)
}
}
} }

View File

@ -42,7 +42,7 @@ sealed class SendMode(open val text: String) {
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
} }
data class TextComposerViewState( data class MessageComposerViewState(
val roomId: String, val roomId: String,
val canSendMessage: Boolean = true, val canSendMessage: Boolean = true,
val isSendButtonVisible: Boolean = false, val isSendButtonVisible: Boolean = false,

View File

@ -103,7 +103,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
voiceMessageViews.renderVisibilityChanged(parentChanged, visibility) voiceMessageViews.renderVisibilityChanged(parentChanged, visibility)
} }
fun display(recordingState: RecordingUiState) { fun render(recordingState: RecordingUiState) {
if (lastKnownState == recordingState) return if (lastKnownState == recordingState) return
lastKnownState = recordingState lastKnownState = recordingState
when (recordingState) { when (recordingState) {

View File

@ -199,7 +199,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<im.vector.app.features.home.room.detail.composer.TextComposerView <im.vector.app.features.home.room.detail.composer.MessageComposerView
android:id="@+id/composerLayout" android:id="@+id/composerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"