diff --git a/changelog.d/3922.feature b/changelog.d/3922.feature new file mode 100644 index 00000000000..bf4e0f7467a --- /dev/null +++ b/changelog.d/3922.feature @@ -0,0 +1 @@ +Voice messages: Persist drafts of voice messages when navigating between rooms \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt index 7ee26de8db3..4ffb816f77e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -22,6 +22,7 @@ import androidx.exifinterface.media.ExifInterface import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType +import org.matrix.android.sdk.internal.di.MoshiProvider @Parcelize @JsonClass(generateAdapter = true) @@ -48,4 +49,14 @@ data class ContentAttachmentData( } fun getSafeMimeType() = mimeType?.normalizeMimeType() + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).toJson(this) + } + + companion object { + fun fromJsonString(json: String): ContentAttachmentData? { + return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).fromJson(json) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt index 9471b3dbcbb..38cee4c7589 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt @@ -23,15 +23,15 @@ package org.matrix.android.sdk.api.session.room.send * EDIT: draft of an edition of a message * REPLY: draft of a reply of another message */ -sealed class UserDraft(open val text: String) { - data class REGULAR(override val text: String) : UserDraft(text) - data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) - data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) - data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) +sealed class UserDraft(open val content: String, open val messageType: String) { + data class REGULAR(override val content: String, override val messageType: String) : UserDraft(content, messageType) + data class QUOTE(val linkedEventId: String, override val content: String, override val messageType: String) : UserDraft(content, messageType) + data class EDIT(val linkedEventId: String, override val content: String, override val messageType: String) : UserDraft(content, messageType) + data class REPLY(val linkedEventId: String, override val content: String, override val messageType: String) : UserDraft(content, messageType) fun isValid(): Boolean { return when (this) { - is REGULAR -> text.isNotBlank() + is REGULAR -> content.isNotBlank() else -> true } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2256d931001..b3eb811a9d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -24,8 +24,10 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.database.model.DraftEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields import org.matrix.android.sdk.internal.database.model.EventEntityFields @@ -381,6 +383,13 @@ internal class RealmSessionStoreMigration @Inject constructor( private fun migrateTo19(realm: DynamicRealm) { Timber.d("Step 18 -> 19") + realm.schema.get("DraftEntity") + ?.addField(DraftEntityFields.MESSAGE_TYPE, String::class.java) + ?.setRequired(DraftEntityFields.MESSAGE_TYPE, true) + ?.transform { + it.setString(DraftEntityFields.MESSAGE_TYPE, MessageType.MSGTYPE_TEXT) + } + realm.schema.get("RoomSummaryEntity") ?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java) ?.transform { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt index 148f727ba71..71350ef94fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.internal.database.model.DraftEntity @@ -26,20 +27,20 @@ internal object DraftMapper { fun map(entity: DraftEntity): UserDraft { return when (entity.draftMode) { - DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) - DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) - DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) - DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content, entity.messageType) + DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content, entity.messageType) + DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content, entity.messageType) + DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content, entity.messageType) else -> null - } ?: UserDraft.REGULAR("") + } ?: UserDraft.REGULAR("", MessageType.MSGTYPE_TEXT) } fun map(domain: UserDraft): DraftEntity { return when (domain) { - is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") - is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) - is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) - is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + is UserDraft.REGULAR -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "", messageType = domain.messageType) + is UserDraft.EDIT -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.QUOTE -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.REPLY -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt index 15a5d379631..eb3f0fadc8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt @@ -17,10 +17,12 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject +import org.matrix.android.sdk.api.session.room.model.message.MessageType internal open class DraftEntity(var content: String = "", var draftMode: String = MODE_REGULAR, - var linkedEventId: String = "" + var linkedEventId: String = "", + var messageType: String = MessageType.MSGTYPE_TEXT ) : RealmObject() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index a9b9f8000b6..7edb692e527 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -110,10 +110,13 @@ sealed class RoomDetailAction : VectorViewModelAction { data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction() // Voice Message + data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : RoomDetailAction() 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() + + data class OnRoomDetailEntersBackground(val isVoiceMessageActive: Boolean) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 75edee5d55e..2ea8c4cb75d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -202,6 +202,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -388,7 +389,13 @@ class RoomDetailFragment @Inject constructor( return@onEach } when (mode) { - is SendMode.REGULAR -> renderRegularMode(mode.text) + is SendMode.REGULAR -> { + if (mode.messageType == MessageType.MSGTYPE_AUDIO) { + renderVoiceMessageMode(mode.text) + } else { + renderRegularMode(mode.text) + } + } is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) @@ -457,6 +464,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + is RoomDetailViewEvents.SaveDraft -> handleSaveDraft(it.defaultContent, it.messageType) }.exhaustive } @@ -466,6 +474,15 @@ class RoomDetailFragment @Inject constructor( } } + private fun renderVoiceMessageMode(content: String) { + ContentAttachmentData.fromJsonString(content)?.let { audioAttachmentData -> + views.voiceMessageRecorderView.isVisible = true + roomDetailViewModel.handle(RoomDetailAction.InitializeVoiceRecorder(audioAttachmentData)) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) + views.voiceMessageRecorderView.initVoiceRecordingViews(isInPlaybackMode = true) + } + } + private fun handleSendButtonVisibilityChanged(event: TextComposerViewEvents.AnimateSendButtonVisibility) { if (event.isVisible) { views.voiceMessageRecorderView.isVisible = false @@ -581,6 +598,20 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleSaveDraft(defaultContent: String?, messageType: String) { + if (messageType == MessageType.MSGTYPE_AUDIO) { + defaultContent?.let { + textComposerViewModel.handle( + TextComposerAction.SaveDraft(it, MessageType.MSGTYPE_AUDIO) + ) + } + } else { + textComposerViewModel.handle( + TextComposerAction.SaveDraft(views.composerLayout.text.toString(), MessageType.MSGTYPE_TEXT) + ) + } + } + private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) { val tag = RoomWidgetPermissionBottomSheet::class.java.name val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet @@ -1015,10 +1046,10 @@ class RoomDetailFragment @Inject constructor( .show() } - private fun renderRegularMode(text: String) { + private fun renderRegularMode(content: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() - views.composerLayout.setTextIfDifferent(text) + views.composerLayout.setTextIfDifferent(content) views.composerLayout.views.sendButton.contentDescription = getString(R.string.send) } @@ -1102,11 +1133,11 @@ class RoomDetailFragment @Inject constructor( notificationDrawerManager.setCurrentRoom(null) - 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)) - views.voiceMessageRecorderView.initVoiceRecordingViews() + roomDetailViewModel.handle( + RoomDetailAction.OnRoomDetailEntersBackground( + isVoiceMessageActive = views.voiceMessageRecorderView.isActive() + ) + ) } private val attachmentFileActivityResultLauncher = registerStartForActivityResult { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 2e7f2bfd638..86ef67e08b8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -81,4 +81,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() + + data class SaveDraft(val defaultContent: String? = null, val messageType: String) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 03bde7d4ccb..a29804ad963 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -72,6 +72,7 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho @@ -86,6 +87,7 @@ import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -342,6 +344,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled) is RoomDetailAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) @@ -354,6 +357,7 @@ class RoomDetailViewModel @AssistedInject constructor( } _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) } + is RoomDetailAction.OnRoomDetailEntersBackground -> handleRoomDetailEntersBackground(action.isVoiceMessageActive) }.exhaustive } @@ -611,9 +615,13 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { + voiceMessageHelper.initializeRecorder(room.roomId, attachmentData) + } + private fun handleStartRecordingVoiceMessage() { try { - voiceMessageHelper.startRecording() + voiceMessageHelper.startRecording(room.roomId) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.Failure(failure)) } @@ -657,6 +665,16 @@ class RoomDetailViewModel @AssistedInject constructor( voiceMessageHelper.stopAllVoiceActions(deleteRecord) } + private fun handleRoomDetailEntersBackground(isVoiceMessageActive: Boolean) { + if (isVoiceMessageActive) { + val audioType = voiceMessageHelper.stopAllVoiceActions(deleteRecord = false) + val audioJsonString = audioType?.toContentAttachmentData()?.toJsonString() + _viewEvents.post(RoomDetailViewEvents.SaveDraft(audioJsonString, MessageType.MSGTYPE_AUDIO)) + } else { + _viewEvents.post(RoomDetailViewEvents.SaveDraft(null, MessageType.MSGTYPE_TEXT)) + } + } + private fun handlePauseRecordingVoiceMessage() { voiceMessageHelper.pauseRecording() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt index 77254001875..c4136457bcd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.composer import im.vector.app.core.platform.VectorViewModelAction sealed class TextComposerAction : VectorViewModelAction { - data class SaveDraft(val draft: String) : TextComposerAction() + data class SaveDraft(val draft: String, val messageType: 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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index e80f25de2f3..445f3e5f1b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -451,20 +451,20 @@ class TextComposerViewModel @AssistedInject constructor( copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (currentDraft) { - is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) + is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.content, false, currentDraft.messageType) is UserDraft.QUOTE -> { room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, currentDraft.text) + SendMode.QUOTE(timelineEvent, currentDraft.content) } } is UserDraft.REPLY -> { room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, currentDraft.text) + SendMode.REPLY(timelineEvent, currentDraft.content) } } is UserDraft.EDIT -> { room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, currentDraft.text) + SendMode.EDIT(timelineEvent, currentDraft.content) } } else -> null @@ -675,20 +675,20 @@ class TextComposerViewModel @AssistedInject constructor( session.coroutineScope.launch { when { it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - setState { copy(sendMode = it.sendMode.copy(action.draft)) } - room.saveDraft(UserDraft.REGULAR(action.draft)) + setState { copy(sendMode = it.sendMode.copy(text = action.draft, messageType = action.messageType)) } + room.saveDraft(UserDraft.REGULAR(action.draft, action.messageType)) } it.sendMode is SendMode.REPLY -> { setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft, action.messageType)) } it.sendMode is SendMode.QUOTE -> { setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft, action.messageType)) } it.sendMode is SendMode.EDIT -> { setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft, action.messageType)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index 199fb1b82d8..6fbdc6c4b9e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MavericksState import im.vector.app.features.home.room.detail.RoomDetailArgs +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -32,6 +33,7 @@ sealed class SendMode(open val text: String) { data class REGULAR( override val text: String, val fromSharing: Boolean, + val messageType: String = MessageType.MSGTYPE_TEXT, // This is necessary for forcing refresh on selectSubscribe private val ts: Long = System.currentTimeMillis() ) : SendMode(text) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index adcd6a3008e..c993f485a14 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -30,6 +30,7 @@ import im.vector.lib.multipicker.entity.MultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber import java.io.File import java.io.FileInputStream @@ -52,13 +53,22 @@ class VoiceMessageHelper @Inject constructor( private var amplitudeTicker: CountUpTimer? = null private var playbackTicker: CountUpTimer? = null - fun startRecording() { + fun initializeRecorder(roomId: String, attachmentData: ContentAttachmentData) { + voiceRecorder.initializeRecord(roomId, attachmentData) + amplitudeList.clear() + attachmentData.waveform?.let { + amplitudeList.addAll(it) + playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList) + } + } + + fun startRecording(roomId: String) { stopPlayback() playbackTracker.makeAllPlaybacksIdle() amplitudeList.clear() try { - voiceRecorder.startRecord() + voiceRecorder.startRecord(roomId) } catch (failure: Throwable) { Timber.e(failure, "Unable to start recording") throw VoiceFailure.UnableToRecord(failure) @@ -217,12 +227,13 @@ class VoiceMessageHelper @Inject constructor( playbackTicker = null } - fun stopAllVoiceActions(deleteRecord: Boolean = true) { - stopRecording() + fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? { + val audioType = stopRecording() stopPlayback() if (deleteRecord) { deleteRecording() } playbackTracker.clear() + return audioType } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index f7b8cead37b..b531a3a5099 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -109,16 +109,22 @@ class VoiceMessageRecorderView : ConstraintLayout, VoiceMessagePlaybackTracker.L } } - fun initVoiceRecordingViews() { - recordingState = RecordingState.NONE + fun initVoiceRecordingViews(isInPlaybackMode: Boolean = false) { + if (isInPlaybackMode) { + recordingState = RecordingState.PLAYBACK - hideRecordingViews(null) - stopRecordingTicker() + showPlaybackViews() + } else { + recordingState = RecordingState.NONE - views.voiceMessageMicButton.isVisible = true - views.voiceMessageSendButton.isVisible = false + hideRecordingViews(null) + stopRecordingTicker() - views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() } + views.voiceMessageMicButton.isVisible = true + views.voiceMessageSendButton.isVisible = false + + views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() } + } } private fun initListeners() { @@ -509,8 +515,10 @@ class VoiceMessageRecorderView : ConstraintLayout, VoiceMessagePlaybackTracker.L } private fun showPlaybackViews() { + views.voiceMessagePlaybackLayout.isVisible = true views.voiceMessagePlaybackTimerIndicator.isVisible = false views.voicePlaybackControlButton.isVisible = true + views.voiceMessageSendButton.isVisible = true views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO callback?.onVoiceRecordingPlaybackModeOn() } diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt index 786920aa223..4a6d3cee8bb 100644 --- a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -19,11 +19,15 @@ package im.vector.app.features.voice import android.content.Context import android.media.MediaRecorder import android.os.Build +import im.vector.app.core.intent.getFilenameFromUri +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.internal.util.md5 import java.io.File import java.io.FileOutputStream +import java.util.UUID abstract class AbstractVoiceRecorder( - context: Context, + private val context: Context, private val filenameExt: String ) : VoiceRecorder { private val outputDirectory: File by lazy { @@ -48,9 +52,22 @@ abstract class AbstractVoiceRecorder( } } - override fun startRecord() { + override fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData) { + getFilenameFromUri(context, attachmentData.queryUri)?.let { + val voiceMessageFolder = File(outputDirectory, roomId.md5()) + outputFile = File(voiceMessageFolder, it) + } + } + + override fun startRecord(roomId: String) { init() - outputFile = File(outputDirectory, "Voice message.$filenameExt") + val fileName = "Voice message.$filenameExt" + val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply { + if (!exists()) { + mkdirs() + } + } + outputFile = File(outputDirectoryForRoom, fileName) val mr = mediaRecorder ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt index 17e70997b21..7adc3efbd8a 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt @@ -16,13 +16,22 @@ package im.vector.app.features.voice +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import java.io.File interface VoiceRecorder { + /** + * Initialize recording with a pre-recorded file. + * @param roomId room id to initialize draft record + * @param attachmentData data of the recorded file + */ + fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData) + /** * Start the recording + * @param roomId id of the room to start record */ - fun startRecord() + fun startRecord(roomId: String) /** * Stop the recording