Skip to content

Commit

Permalink
Merge pull request #4558 from vector-im/feature/adm/voice-draft
Browse files Browse the repository at this point in the history
Adding support for voice drafts
  • Loading branch information
bmarty authored Nov 30, 2021
2 parents 02a6091 + aaba628 commit 3a8fd42
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 83 deletions.
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 Down Expand Up @@ -49,4 +50,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 @@ -24,14 +24,15 @@ package org.matrix.android.sdk.api.session.room.send
* REPLY: draft of a reply of another message
*/
sealed interface UserDraft {
data class Regular(val text: String) : UserDraft
data class Quote(val linkedEventId: String, val text: String) : UserDraft
data class Edit(val linkedEventId: String, val text: String) : UserDraft
data class Reply(val linkedEventId: String, val text: String) : UserDraft
data class Regular(val content: String) : UserDraft
data class Quote(val linkedEventId: String, val content: String) : UserDraft
data class Edit(val linkedEventId: String, val content: String) : UserDraft
data class Reply(val linkedEventId: String, val content: String) : UserDraft
data class Voice(val content: String) : UserDraft

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 @@ -30,16 +30,18 @@ internal object DraftMapper {
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_VOICE -> UserDraft.Voice(entity.content)
else -> null
} ?: UserDraft.Regular("")
}

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 = "")
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)
is UserDraft.Voice -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_VOICE, linkedEventId = "")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import io.realm.RealmObject
internal open class DraftEntity(var content: String = "",
var draftMode: String = MODE_REGULAR,
var linkedEventId: String = ""

) : RealmObject() {

companion object {
const val MODE_REGULAR = "REGULAR"
const val MODE_EDIT = "EDIT"
const val MODE_REPLY = "REPLY"
const val MODE_QUOTE = "QUOTE"
const val MODE_VOICE = "VOICE"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ class RoomDetailFragment @Inject constructor(
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)
is SendMode.Voice -> renderVoiceMessageMode(mode.text)
}
}

Expand Down Expand Up @@ -471,6 +472,13 @@ class RoomDetailFragment @Inject constructor(
}
}

private fun renderVoiceMessageMode(content: String) {
ContentAttachmentData.fromJsonString(content)?.let { audioAttachmentData ->
views.voiceMessageRecorderView.isVisible = true
messageComposerViewModel.handle(MessageComposerAction.InitializeVoiceRecorder(audioAttachmentData))
}
}

private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
if (event.isVisible) {
views.voiceMessageRecorderView.isVisible = false
Expand Down Expand Up @@ -507,7 +515,7 @@ class RoomDetailFragment @Inject constructor(

private fun onCannotRecord() {
// Update the UI, cancel the animation
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.Idle))
}

private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
Expand Down Expand Up @@ -701,7 +709,7 @@ class RoomDetailFragment @Inject constructor(
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
vibrate(requireContext())
updateRecordingUiState(RecordingUiState.Started(clock.epochMillis()))
updateRecordingUiState(RecordingUiState.Recording(clock.epochMillis()))
}
}

Expand All @@ -711,11 +719,12 @@ class RoomDetailFragment @Inject constructor(

override fun onVoiceRecordingCancelled() {
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.Cancelled)
vibrate(requireContext())
updateRecordingUiState(RecordingUiState.Idle)
}

override fun onVoiceRecordingLocked() {
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Started }
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Recording }
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
updateRecordingUiState(RecordingUiState.Locked(startTime))
}
Expand All @@ -726,22 +735,22 @@ class RoomDetailFragment @Inject constructor(

override fun onSendVoiceMessage() {
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
updateRecordingUiState(RecordingUiState.None)
updateRecordingUiState(RecordingUiState.Idle)
}

override fun onDeleteVoiceMessage() {
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
updateRecordingUiState(RecordingUiState.None)
updateRecordingUiState(RecordingUiState.Idle)
}

override fun onRecordingLimitReached() {
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
updateRecordingUiState(RecordingUiState.Draft)
}

override fun onRecordingWaveformClicked() {
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
updateRecordingUiState(RecordingUiState.Playback)
updateRecordingUiState(RecordingUiState.Draft)
}

private fun updateRecordingUiState(state: RecordingUiState) {
Expand Down Expand Up @@ -1046,10 +1055,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 @@ -1139,10 +1148,7 @@ class RoomDetailFragment @Inject constructor(
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
// we're rotating, maintain any active recordings
} else {
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.
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
views.voiceMessageRecorderView.render(RecordingUiState.None)
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(views.composerLayout.text.toString()))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ 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.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent

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()
data class OnEntersBackground(val composerText: String) : MessageComposerAction()

// Voice Message
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
object StartRecordingVoiceMessage : MessageComposerAction()
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
Expand Down
Loading

0 comments on commit 3a8fd42

Please sign in to comment.