moving voice recording logic to the TextComposerViewModel (name to be updated) from the RoomDetailViewModel

This commit is contained in:
Adam Brown 2021-11-19 14:24:04 +00:00
parent 35f9bef94a
commit b5055453d1
7 changed files with 119 additions and 116 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

@ -241,7 +241,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleterFactory: AutoCompleter.Factory,
private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val textComposerViewModelFactory: TextComposerViewModel.Factory,
private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider,
@ -414,23 +414,24 @@ class RoomDetailFragment @Inject constructor(
textComposerViewModel.observeViewEvents {
when (it) {
is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
is TextComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
}.exhaustive
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> {
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)
is TextComposerViewEvents.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)
@ -698,18 +699,18 @@ class RoomDetailFragment @Inject constructor(
override fun onVoiceRecordingStarted() {
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
textComposerViewModel.handle(TextComposerAction.StartRecordingVoiceMessage)
vibrate(requireContext())
updateRecordingUiState(RecordingUiState.Started)
}
}
override fun onVoicePlaybackButtonClicked() {
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
textComposerViewModel.handle(TextComposerAction.PlayOrPauseRecordingPlayback)
}
override fun onVoiceRecordingCancelled() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
textComposerViewModel.handle(TextComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled)
}
@ -722,22 +723,22 @@ class RoomDetailFragment @Inject constructor(
}
override fun onSendVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = false))
textComposerViewModel.handle(TextComposerAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None)
}
override fun onDeleteVoiceMessage() {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled = true))
textComposerViewModel.handle(TextComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.None)
}
override fun onRecordingLimitReached() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
textComposerViewModel.handle(TextComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
}
override fun onRecordingWaveformClicked() {
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
textComposerViewModel.handle(TextComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
}
@ -1136,7 +1137,7 @@ class RoomDetailFragment @Inject constructor(
textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString()))
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions(deleteRecord = false))
textComposerViewModel.handle(TextComposerAction.EndAllVoiceActions(deleteRecord = false))
views.voiceMessageRecorderView.display(RecordingUiState.None)
}
@ -1883,7 +1884,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) {
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
textComposerViewModel.handle(TextComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
}
private fun onShareActionClicked(action: EventSharedAction.Share) {

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,6 +18,7 @@ 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()
@ -28,5 +29,13 @@ sealed class TextComposerAction : VectorViewModelAction {
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction()
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
data class OnTextChanged(val text: CharSequence) : TextComposerAction()
// Voice Message
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : TextComposerAction()
object StartRecordingVoiceMessage : TextComposerAction()
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : TextComposerAction()
object PauseRecordingVoiceMessage : TextComposerAction()
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : TextComposerAction()
object PlayOrPauseRecordingPlayback : TextComposerAction()
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : TextComposerAction()
}

View File

@ -42,4 +42,6 @@ sealed class TextComposerViewEvents : VectorViewEvents {
object SlashCommandNotImplemented : SendMessageResult()
data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents()
data class VoicePlaybackOrRecordingFailure(val throwable: Throwable) : TextComposerViewEvents()
}

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
@ -60,7 +63,9 @@ class TextComposerViewModel @AssistedInject constructor(
private val session: Session,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val rainbowGenerator: RainbowGenerator
private val rainbowGenerator: RainbowGenerator,
private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<TextComposerViewState, TextComposerAction, TextComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
@ -86,6 +91,12 @@ class TextComposerViewModel @AssistedInject constructor(
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
is TextComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
TextComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is TextComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
is TextComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
TextComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
TextComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is TextComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
}
}
@ -688,6 +699,56 @@ class TextComposerViewModel @AssistedInject constructor(
}
}
private fun handleStartRecordingVoiceMessage() {
try {
voiceMessageHelper.startRecording()
} catch (failure: Throwable) {
_viewEvents.post(TextComposerViewEvents.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: TextComposerAction.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(TextComposerViewEvents.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)
viewModelScope.launch {
@ -703,9 +764,19 @@ class TextComposerViewModel @AssistedInject constructor(
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<TextComposerViewModel, TextComposerViewState> {
override fun create(initialState: TextComposerViewState): TextComposerViewModel
interface Factory {
fun create(initialState: TextComposerViewState): TextComposerViewModel
}
companion object : MavericksViewModelFactory<TextComposerViewModel, TextComposerViewState> 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<TextComposerViewModel, TextComposerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel {
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.textComposerViewModelFactory.create(state)
}
}
}