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.UnreadMessagesSharedViewModel
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.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -505,8 +505,8 @@ interface MavericksViewModelModule {
@Binds
@IntoMap
@MavericksViewModelKey(TextComposerViewModel::class)
fun textComposerViewModelFactory(factory: TextComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@MavericksViewModelKey(RoomDetailViewModel::class)
fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap

View File

@ -21,7 +21,6 @@ import android.view.View
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.conference.ConferenceEvent
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.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -108,12 +107,4 @@ sealed class RoomDetailAction : VectorViewModelAction {
object RemoveAllFailedMessages : 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.verification.VerificationBottomSheet
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.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.RecordingUiState
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
@ -240,7 +240,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleterFactory: AutoCompleter.Factory,
private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val messageComposerViewModelFactory: MessageComposerViewModel.Factory,
private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider,
@ -293,7 +293,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleterFactory.create(roomDetailArgs.roomId)
}
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val messageComposerViewModel: MessageComposerViewModel by fragmentViewModel()
private val debouncer = Debouncer(createUIHandler())
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@ -386,7 +386,7 @@ class RoomDetailFragment @Inject constructor(
updateJumpToReadMarkerViewVisibility()
}
textComposerViewModel.onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend ->
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
if (!canSend) {
return@onEach
}
@ -411,25 +411,26 @@ class RoomDetailFragment @Inject constructor(
)
}
textComposerViewModel.observeViewEvents {
messageComposerViewModel.observeViewEvents {
when (it) {
is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
is TextComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
}.exhaustive
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> {
is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
if (it.throwable is VoiceFailure.UnableToRecord) {
onCannotRecord()
}
showErrorInSnackbar(it.throwable)
}
}.exhaustive
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(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) {
views.voiceMessageRecorderView.isVisible = false
views.composerLayout.views.sendButton.alpha = 0f
@ -505,7 +506,7 @@ class RoomDetailFragment @Inject constructor(
private fun onCannotRecord() {
// Update the UI, cancel the animation
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
}
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
@ -524,7 +525,7 @@ class RoomDetailFragment @Inject constructor(
JoinReplacementRoomBottomSheet().show(childFragmentManager, tag)
}
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) {
private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: MessageComposerViewEvents.ShowRoomUpgradeDialog) {
val tag = MigrateRoomBottomSheet::javaClass.name
MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion)
.show(parentFragmentManager, tag)
@ -697,18 +698,18 @@ class RoomDetailFragment @Inject constructor(
override fun onVoiceRecordingStarted() {
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
vibrate(requireContext())
updateRecordingUiState(RecordingUiState.Started)
}
}
override fun onVoicePlaybackButtonClicked() {
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseRecordingPlayback)
}
override fun onVoiceRecordingCancelled() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled)
}
@ -721,27 +722,27 @@ class RoomDetailFragment @Inject constructor(
}
override fun onSendVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false))
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None)
}
override fun onDeleteVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.None)
}
override fun onRecordingLimitReached() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
}
override fun onRecordingWaveformClicked() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
}
private fun updateRecordingUiState(state: RecordingUiState) {
textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingUiStateChanged(state))
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
}
}
}
@ -818,7 +819,7 @@ class RoomDetailFragment @Inject constructor(
.show()
}
private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) {
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
views.composerLayout.setTextIfDifferent("")
lockSendButton = false
navigator.openRoom(vectorBaseActivity, action.roomId)
@ -827,7 +828,7 @@ class RoomDetailFragment @Inject constructor(
private fun handleShareData() {
when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> {
textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
}
is SharedData.Attachments -> {
// open share edition
@ -1132,11 +1133,11 @@ class RoomDetailFragment @Inject constructor(
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.
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions(deleteRecord = false))
views.voiceMessageRecorderView.display(RecordingUiState.None)
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
views.voiceMessageRecorderView.render(RecordingUiState.None)
}
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
@ -1251,12 +1252,12 @@ class RoomDetailFragment @Inject constructor(
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let {
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 {
val canSendMessage = withState(textComposerViewModel) {
val canSendMessage = withState(messageComposerViewModel) {
it.canSendMessage
}
if (!canSendMessage) {
@ -1345,7 +1346,7 @@ class RoomDetailFragment @Inject constructor(
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
views.composerLayout.callback = object : TextComposerView.Callback {
views.composerLayout.callback = object : MessageComposerView.Callback {
override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) {
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
@ -1358,7 +1359,7 @@ class RoomDetailFragment @Inject constructor(
}
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 {
@ -1366,7 +1367,7 @@ class RoomDetailFragment @Inject constructor(
}
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
views.composerLayout.collapse(true)
lockSendButton = true
textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
emojiPopup.dismiss()
}
}
@ -1392,7 +1393,7 @@ class RoomDetailFragment @Inject constructor(
.map { it.isNotEmpty() }
.onEach {
Timber.d("Typing: User is typing: $it")
textComposerViewModel.handle(TextComposerAction.UserIsTyping(it))
messageComposerViewModel.handle(MessageComposerAction.UserIsTyping(it))
}
.launchIn(viewLifecycleOwner.lifecycleScope)
@ -1412,7 +1413,7 @@ class RoomDetailFragment @Inject constructor(
return isHandled
}
override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState ->
override fun invalidate() = withState(roomDetailViewModel, messageComposerViewModel) { mainState, messageComposerState ->
invalidateOptionsMenu()
val summary = mainState.asyncRoomSummary()
renderToolbar(summary, mainState.formattedTypingUsers)
@ -1429,13 +1430,13 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.update(mainState)
lazyLoadedViews.inviteView(false)?.isVisible = false
if (mainState.tombstoneEvent == null) {
views.composerLayout.isInvisible = !textComposerState.isComposerVisible
views.voiceMessageRecorderView.isVisible = textComposerState.isVoiceMessageRecorderVisible
views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible
views.voiceMessageRecorderView.display(textComposerState.voiceRecordingUiState)
views.composerLayout.isInvisible = !messageComposerState.isComposerVisible
views.voiceMessageRecorderView.isVisible = messageComposerState.isVoiceMessageRecorderVisible
views.composerLayout.views.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState)
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
// views.composerLayout.alwaysShowSendButton = false
if (textComposerState.canSendMessage) {
if (messageComposerState.canSendMessage) {
views.notificationAreaView.render(NotificationAreaView.State.Hidden)
} else {
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) {
is TextComposerViewEvents.SlashCommandLoading -> {
is MessageComposerViewEvents.SlashCommandLoading -> {
showLoading(null)
}
is TextComposerViewEvents.SlashCommandError -> {
is MessageComposerViewEvents.SlashCommandError -> {
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))
}
is TextComposerViewEvents.SlashCommandResultOk -> {
is MessageComposerViewEvents.SlashCommandResultOk -> {
dismissLoadingDialog()
views.composerLayout.setTextIfDifferent("")
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
}
is TextComposerViewEvents.SlashCommandResultError -> {
is MessageComposerViewEvents.SlashCommandResultError -> {
dismissLoadingDialog()
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
}
is TextComposerViewEvents.SlashCommandNotImplemented -> {
is MessageComposerViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
}
} // .exhaustive
@ -1883,7 +1884,7 @@ class RoomDetailFragment @Inject constructor(
}
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) {
@ -1988,18 +1989,18 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
is EventSharedAction.Edit -> {
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) {
textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
} else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
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 -> {
if (withState(textComposerViewModel) { it.isVoiceMessageIdle }) {
textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
messageComposerViewModel.handle(MessageComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
} else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
@ -2212,7 +2213,7 @@ class RoomDetailFragment @Inject constructor(
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
super.onContactAttachmentReady(contactAttachment)
val formattedContact = contactAttachment.toHumanReadable()
textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false))
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
}
private fun onViewWidgetsClicked() {

View File

@ -21,24 +21,23 @@ import androidx.annotation.IdRes
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModelFactory
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.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.flow.chunk
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
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.JitsiActiveConferenceHolder
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.crypto.keysrequest.OutboundSessionKeySharingStrategy
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.timeline.factory.TimelineFactory
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.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoicePlayerHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
@ -116,8 +113,6 @@ class RoomDetailViewModel @AssistedInject constructor(
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper,
timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -144,22 +139,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private var prepareToEncrypt: Async<Unit> = Uninitialized
@AssistedFactory
interface Factory {
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
interface Factory : MavericksAssistedViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
override fun create(initialState: RoomDetailViewState): RoomDetailViewModel
}
/**
* 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> {
companion object : MavericksViewModelFactory<RoomDetailViewModel, RoomDetailViewState> by hiltMavericksViewModelFactory() {
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 {
@ -343,12 +328,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
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 -> {
setState {
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()
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.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
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()
data class OnTextChanged(val text: CharSequence) : TextComposerAction()
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : TextComposerAction()
sealed class MessageComposerAction : VectorViewModelAction {
data class SaveDraft(val draft: String) : MessageComposerAction()
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterReplyMode(val eventId: String, val text: String) : MessageComposerAction()
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
// 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.
*/
class TextComposerView @JvmOverloads constructor(
class MessageComposerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
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.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()
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
@ -36,10 +36,12 @@ sealed class TextComposerViewEvents : VectorViewEvents {
data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
data class OpenRoomMemberProfile(val userId: String) : TextComposerViewEvents()
data class OpenRoomMemberProfile(val userId: String) : MessageComposerViewEvents()
// TODO Remove
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
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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.platform.VectorViewModel
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.ParsedCommand
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.toMessageType
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoicePlayerHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 timber.log.Timber
class TextComposerViewModel @AssistedInject constructor(
@Assisted initialState: TextComposerViewState,
class MessageComposerViewModel @AssistedInject constructor(
@Assisted initialState: MessageComposerViewState,
private val session: Session,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val rainbowGenerator: RainbowGenerator
) : VectorViewModel<TextComposerViewState, TextComposerAction, TextComposerViewEvents>(initialState) {
private val rainbowGenerator: RainbowGenerator,
private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
@ -74,26 +79,32 @@ class TextComposerViewModel @AssistedInject constructor(
subscribeToStateInternal()
}
override fun handle(action: TextComposerAction) {
override fun handle(action: MessageComposerAction) {
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)
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
is TextComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
is MessageComposerAction.SaveDraft -> handleSaveDraft(action)
is MessageComposerAction.SendMessage -> handleSendMessage(action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(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)
}
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
setState {
// Makes sure currentComposerText is upToDate when accessing further setState
currentComposerText = action.text
@ -103,7 +114,7 @@ class TextComposerViewModel @AssistedInject constructor(
}
private fun subscribeToStateInternal() {
onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ ->
onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage, MessageComposerViewState::isVoiceRecording) { _, _, _ ->
updateIsSendButtonVisibility(false)
}
}
@ -111,16 +122,16 @@ class TextComposerViewModel @AssistedInject constructor(
private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState {
val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank())
if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) {
_viewEvents.post(TextComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible))
_viewEvents.post(MessageComposerViewEvents.AnimateSendButtonVisibility(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))
}
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
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 ->
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 ->
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
}
}
private fun handleSendMessage(action: TextComposerAction.SendMessage) {
private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
withState { state ->
when (state.sendMode) {
is SendMode.REGULAR -> {
@ -154,22 +165,22 @@ class TextComposerViewModel @AssistedInject constructor(
is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command))
_viewEvents.post(MessageComposerViewEvents.SlashCommandError(slashCommandResult.command))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/"))
_viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown("/"))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
_viewEvents.post(MessageComposerViewEvents.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)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ChangeRoomName -> {
@ -186,11 +197,11 @@ class TextComposerViewModel @AssistedInject constructor(
}
is ParsedCommand.ClearScalarToken -> {
// TODO
_viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented)
_viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
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))
popDraft()
}
@ -218,21 +229,21 @@ class TextComposerViewModel @AssistedInject constructor(
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
}
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
}
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendSpoiler -> {
@ -240,22 +251,22 @@ class TextComposerViewModel @AssistedInject constructor(
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.SendChatEffect -> {
sendChatEffect(slashCommandResult)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
@ -274,25 +285,25 @@ class TextComposerViewModel @AssistedInject constructor(
handleChangeAvatarForRoomSlashCommand(slashCommandResult)
}
is ParsedCommand.ShowUser -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
handleWhoisSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.DiscardSession -> {
if (room.isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
} else {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(
TextComposerViewEvents
MessageComposerViewEvents
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
)
}
}
is ParsedCommand.CreateSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) {
try {
val params = CreateSpaceParams().apply {
@ -308,15 +319,15 @@ class TextComposerViewModel @AssistedInject constructor(
true
)
popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
}
}
Unit
}
is ParsedCommand.AddToSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().getSpace(slashCommandResult.spaceId)
@ -327,22 +338,22 @@ class TextComposerViewModel @AssistedInject constructor(
false
)
popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
}
}
Unit
}
is ParsedCommand.JoinSpace -> {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
}
}
Unit
@ -352,21 +363,21 @@ class TextComposerViewModel @AssistedInject constructor(
try {
session.getRoom(slashCommandResult.roomId)?.leave(null)
popDraft()
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
}
}
Unit
}
is ParsedCommand.UpgradeRoom -> {
_viewEvents.post(
TextComposerViewEvents.ShowRoomUpgradeDialog(
MessageComposerViewEvents.ShowRoomUpgradeDialog(
slashCommandResult.newVersion,
room.roomSummary()?.isPublic ?: false
)
)
_viewEvents.post(TextComposerViewEvents.SlashCommandResultOk())
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
popDraft()
}
}.exhaustive
@ -391,7 +402,7 @@ class TextComposerViewModel @AssistedInject constructor(
Timber.w("Same message content, do not send edition")
}
}
_viewEvents.post(TextComposerViewEvents.MessageSent)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
@ -412,13 +423,13 @@ class TextComposerViewModel @AssistedInject constructor(
} else {
room.sendFormattedTextMessage(finalText, htmlText)
}
_viewEvents.post(TextComposerViewEvents.MessageSent)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(TextComposerViewEvents.MessageSent)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
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 (action.isTyping) {
room.userIsTyping()
@ -495,13 +506,13 @@ class TextComposerViewModel @AssistedInject constructor(
try {
session.joinRoom(command.roomAlias, command.reason, emptyList())
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure))
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
return@launch
}
session.getRoomSummary(command.roomAlias)
?.roomId
?.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) {
_viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId))
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
}
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
*/
private fun handleSaveDraft(action: TextComposerAction.SaveDraft) = withState {
private fun handleSaveDraft(action: MessageComposerAction.SaveDraft) = withState {
session.coroutineScope.launch {
when {
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) {
_viewEvents.post(TextComposerViewEvents.SlashCommandLoading)
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch {
val event = try {
block()
popDraft()
TextComposerViewEvents.SlashCommandResultOk()
MessageComposerViewEvents.SlashCommandResultOk()
} catch (failure: Throwable) {
TextComposerViewEvents.SlashCommandResultError(failure)
MessageComposerViewEvents.SlashCommandResultError(failure)
}
_viewEvents.post(event)
}
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<TextComposerViewModel, TextComposerViewState> {
override fun create(initialState: TextComposerViewState): TextComposerViewModel
interface Factory {
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 TextComposerViewState(
data class MessageComposerViewState(
val roomId: String,
val canSendMessage: Boolean = true,
val isSendButtonVisible: Boolean = false,

View File

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

View File

@ -199,7 +199,7 @@
app:layout_constraintEnd_toEndOf="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:layout_width="match_parent"
android:layout_height="wrap_content"