Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice Message - Draft support #4237

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3922.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Voice messages: Persist drafts of voice messages when navigating between rooms
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a problem for those who are already on version 19. In this case you should create a version 20.

realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java)
?.transform {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not talk to the textComposerViewModel anymore here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

roomDetailViewModel will decide it and communicate with textComposerViewModel later with SaveDraft event.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

RoomDetailAction.OnRoomDetailEntersBackground(
isVoiceMessageActive = views.voiceMessageRecorderView.isActive()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is strange to ask the view about the state. The ViewModel should be aware of the current state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it complex to determine the state here, especially if there is already a draft in the room.

)
)
}

private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -354,6 +357,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
}
is RoomDetailAction.OnRoomDetailEntersBackground -> handleRoomDetailEntersBackground(action.isVoiceMessageActive)
}.exhaustive
}

Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird arch

}
}

private fun handlePauseRecordingVoiceMessage() {
voiceMessageHelper.pauseRecording()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading