Merge pull request #4558 from vector-im/feature/adm/voice-draft
Adding support for voice drafts
This commit is contained in:
commit
3a8fd42513
1
changelog.d/3922.feature
Normal file
1
changelog.d/3922.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Voice messages: Persist drafts of voice messages when navigating between rooms
|
@ -22,6 +22,7 @@ import androidx.exifinterface.media.ExifInterface
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@ -49,4 +50,14 @@ data class ContentAttachmentData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,14 +24,15 @@ package org.matrix.android.sdk.api.session.room.send
|
|||||||
* REPLY: draft of a reply of another message
|
* REPLY: draft of a reply of another message
|
||||||
*/
|
*/
|
||||||
sealed interface UserDraft {
|
sealed interface UserDraft {
|
||||||
data class Regular(val text: String) : UserDraft
|
data class Regular(val content: String) : UserDraft
|
||||||
data class Quote(val linkedEventId: String, val text: String) : UserDraft
|
data class Quote(val linkedEventId: String, val content: String) : UserDraft
|
||||||
data class Edit(val linkedEventId: String, val text: String) : UserDraft
|
data class Edit(val linkedEventId: String, val content: String) : UserDraft
|
||||||
data class Reply(val linkedEventId: String, val text: String) : UserDraft
|
data class Reply(val linkedEventId: String, val content: String) : UserDraft
|
||||||
|
data class Voice(val content: String) : UserDraft
|
||||||
|
|
||||||
fun isValid(): Boolean {
|
fun isValid(): Boolean {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Regular -> text.isNotBlank()
|
is Regular -> content.isNotBlank()
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,16 +30,18 @@ internal object DraftMapper {
|
|||||||
DraftEntity.MODE_EDIT -> UserDraft.Edit(entity.linkedEventId, entity.content)
|
DraftEntity.MODE_EDIT -> UserDraft.Edit(entity.linkedEventId, entity.content)
|
||||||
DraftEntity.MODE_QUOTE -> UserDraft.Quote(entity.linkedEventId, entity.content)
|
DraftEntity.MODE_QUOTE -> UserDraft.Quote(entity.linkedEventId, entity.content)
|
||||||
DraftEntity.MODE_REPLY -> UserDraft.Reply(entity.linkedEventId, entity.content)
|
DraftEntity.MODE_REPLY -> UserDraft.Reply(entity.linkedEventId, entity.content)
|
||||||
|
DraftEntity.MODE_VOICE -> UserDraft.Voice(entity.content)
|
||||||
else -> null
|
else -> null
|
||||||
} ?: UserDraft.Regular("")
|
} ?: UserDraft.Regular("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun map(domain: UserDraft): DraftEntity {
|
fun map(domain: UserDraft): DraftEntity {
|
||||||
return when (domain) {
|
return when (domain) {
|
||||||
is UserDraft.Regular -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
|
is UserDraft.Regular -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
|
||||||
is UserDraft.Edit -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
|
is UserDraft.Edit -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
|
||||||
is UserDraft.Quote -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
|
is UserDraft.Quote -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
|
||||||
is UserDraft.Reply -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, 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 = "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import io.realm.RealmObject
|
|||||||
internal open class DraftEntity(var content: String = "",
|
internal open class DraftEntity(var content: String = "",
|
||||||
var draftMode: String = MODE_REGULAR,
|
var draftMode: String = MODE_REGULAR,
|
||||||
var linkedEventId: String = ""
|
var linkedEventId: String = ""
|
||||||
|
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -29,5 +28,6 @@ internal open class DraftEntity(var content: String = "",
|
|||||||
const val MODE_EDIT = "EDIT"
|
const val MODE_EDIT = "EDIT"
|
||||||
const val MODE_REPLY = "REPLY"
|
const val MODE_REPLY = "REPLY"
|
||||||
const val MODE_QUOTE = "QUOTE"
|
const val MODE_QUOTE = "QUOTE"
|
||||||
|
const val MODE_VOICE = "VOICE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.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.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.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||||
|
is SendMode.Voice -> renderVoiceMessageMode(mode.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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) {
|
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
||||||
if (event.isVisible) {
|
if (event.isVisible) {
|
||||||
views.voiceMessageRecorderView.isVisible = false
|
views.voiceMessageRecorderView.isVisible = false
|
||||||
@ -507,7 +515,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
private fun onCannotRecord() {
|
private fun onCannotRecord() {
|
||||||
// Update the UI, cancel the animation
|
// Update the UI, cancel the animation
|
||||||
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
|
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.Idle))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
||||||
@ -701,7 +709,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
||||||
vibrate(requireContext())
|
vibrate(requireContext())
|
||||||
updateRecordingUiState(RecordingUiState.Started(clock.epochMillis()))
|
updateRecordingUiState(RecordingUiState.Recording(clock.epochMillis()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,11 +719,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun onVoiceRecordingCancelled() {
|
override fun onVoiceRecordingCancelled() {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||||
updateRecordingUiState(RecordingUiState.Cancelled)
|
vibrate(requireContext())
|
||||||
|
updateRecordingUiState(RecordingUiState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoiceRecordingLocked() {
|
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()
|
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
|
||||||
updateRecordingUiState(RecordingUiState.Locked(startTime))
|
updateRecordingUiState(RecordingUiState.Locked(startTime))
|
||||||
}
|
}
|
||||||
@ -726,22 +735,22 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun onSendVoiceMessage() {
|
override fun onSendVoiceMessage() {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
|
||||||
updateRecordingUiState(RecordingUiState.None)
|
updateRecordingUiState(RecordingUiState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteVoiceMessage() {
|
override fun onDeleteVoiceMessage() {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||||
updateRecordingUiState(RecordingUiState.None)
|
updateRecordingUiState(RecordingUiState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecordingLimitReached() {
|
override fun onRecordingLimitReached() {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||||
updateRecordingUiState(RecordingUiState.Playback)
|
updateRecordingUiState(RecordingUiState.Draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecordingWaveformClicked() {
|
override fun onRecordingWaveformClicked() {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||||
updateRecordingUiState(RecordingUiState.Playback)
|
updateRecordingUiState(RecordingUiState.Draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRecordingUiState(state: RecordingUiState) {
|
private fun updateRecordingUiState(state: RecordingUiState) {
|
||||||
@ -1046,10 +1055,10 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderRegularMode(text: String) {
|
private fun renderRegularMode(content: String) {
|
||||||
autoCompleter.exitSpecialMode()
|
autoCompleter.exitSpecialMode()
|
||||||
views.composerLayout.collapse()
|
views.composerLayout.collapse()
|
||||||
views.composerLayout.setTextIfDifferent(text)
|
views.composerLayout.setTextIfDifferent(content)
|
||||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1139,10 +1148,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||||
// we're rotating, maintain any active recordings
|
// we're rotating, maintain any active recordings
|
||||||
} else {
|
} else {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.composer
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
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
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
|
|
||||||
sealed class MessageComposerAction : VectorViewModelAction {
|
sealed class MessageComposerAction : VectorViewModelAction {
|
||||||
data class SaveDraft(val draft: String) : MessageComposerAction()
|
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
||||||
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
|
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||||
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
|
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||||
@ -29,8 +29,10 @@ sealed class MessageComposerAction : VectorViewModelAction {
|
|||||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
|
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
|
||||||
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
||||||
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
||||||
|
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
|
||||||
|
|
||||||
// Voice Message
|
// Voice Message
|
||||||
|
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
||||||
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
||||||
object StartRecordingVoiceMessage : MessageComposerAction()
|
object StartRecordingVoiceMessage : MessageComposerAction()
|
||||||
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
|
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
|
||||||
|
@ -31,6 +31,7 @@ import im.vector.app.features.command.CommandParser
|
|||||||
import im.vector.app.features.command.ParsedCommand
|
import im.vector.app.features.command.ParsedCommand
|
||||||
import im.vector.app.features.home.room.detail.ChatEffect
|
import im.vector.app.features.home.room.detail.ChatEffect
|
||||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||||
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||||
import im.vector.app.features.home.room.detail.toMessageType
|
import im.vector.app.features.home.room.detail.toMessageType
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
@ -42,6 +43,7 @@ import org.commonmark.parser.Parser
|
|||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
@ -85,17 +87,18 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||||
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||||
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
||||||
is MessageComposerAction.SaveDraft -> handleSaveDraft(action)
|
|
||||||
is MessageComposerAction.SendMessage -> handleSendMessage(action)
|
is MessageComposerAction.SendMessage -> handleSendMessage(action)
|
||||||
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||||
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
||||||
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
|
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
|
||||||
MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
||||||
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
||||||
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
||||||
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
||||||
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
||||||
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
|
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
|
||||||
|
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
|
||||||
|
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,6 +435,9 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is SendMode.Voice -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -455,22 +461,23 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
copy(
|
copy(
|
||||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||||
sendMode = when (currentDraft) {
|
sendMode = when (currentDraft) {
|
||||||
is UserDraft.Regular -> SendMode.Regular(currentDraft.text, false)
|
is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false)
|
||||||
is UserDraft.Quote -> {
|
is UserDraft.Quote -> {
|
||||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.Quote(timelineEvent, currentDraft.text)
|
SendMode.Quote(timelineEvent, currentDraft.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserDraft.Reply -> {
|
is UserDraft.Reply -> {
|
||||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.Reply(timelineEvent, currentDraft.text)
|
SendMode.Reply(timelineEvent, currentDraft.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserDraft.Edit -> {
|
is UserDraft.Edit -> {
|
||||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.Edit(timelineEvent, currentDraft.text)
|
SendMode.Edit(timelineEvent, currentDraft.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is UserDraft.Voice -> SendMode.Voice(currentDraft.content)
|
||||||
else -> null
|
else -> null
|
||||||
} ?: SendMode.Regular("", fromSharing = false)
|
} ?: SendMode.Regular("", fromSharing = false)
|
||||||
)
|
)
|
||||||
@ -675,24 +682,24 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
/**
|
/**
|
||||||
* Convert a send mode to a draft and save the draft
|
* Convert a send mode to a draft and save the draft
|
||||||
*/
|
*/
|
||||||
private fun handleSaveDraft(action: MessageComposerAction.SaveDraft) = withState {
|
private fun handleSaveTextDraft(draft: String) = withState {
|
||||||
session.coroutineScope.launch {
|
session.coroutineScope.launch {
|
||||||
when {
|
when {
|
||||||
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
|
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
|
||||||
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
|
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||||
room.saveDraft(UserDraft.Regular(action.draft))
|
room.saveDraft(UserDraft.Regular(draft))
|
||||||
}
|
}
|
||||||
it.sendMode is SendMode.Reply -> {
|
it.sendMode is SendMode.Reply -> {
|
||||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||||
room.saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
room.saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||||
}
|
}
|
||||||
it.sendMode is SendMode.Quote -> {
|
it.sendMode is SendMode.Quote -> {
|
||||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||||
room.saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
room.saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||||
}
|
}
|
||||||
it.sendMode is SendMode.Edit -> {
|
it.sendMode is SendMode.Edit -> {
|
||||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||||
room.saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
room.saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -700,7 +707,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun handleStartRecordingVoiceMessage() {
|
private fun handleStartRecordingVoiceMessage() {
|
||||||
try {
|
try {
|
||||||
voiceMessageHelper.startRecording()
|
voiceMessageHelper.startRecording(room.roomId)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||||
}
|
}
|
||||||
@ -719,6 +726,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
||||||
@ -741,13 +749,35 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
||||||
|
voiceMessageHelper.clearTracker()
|
||||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
||||||
|
voiceMessageHelper.initializeRecorder(attachmentData)
|
||||||
|
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePauseRecordingVoiceMessage() {
|
private fun handlePauseRecordingVoiceMessage() {
|
||||||
voiceMessageHelper.pauseRecording()
|
voiceMessageHelper.pauseRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleEntersBackground(composerText: String) {
|
||||||
|
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
||||||
|
if (isVoiceRecording) {
|
||||||
|
voiceMessageHelper.clearTracker()
|
||||||
|
viewModelScope.launch {
|
||||||
|
voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)?.toContentAttachmentData()?.let { voiceDraft ->
|
||||||
|
val content = voiceDraft.toJsonString()
|
||||||
|
room.saveDraft(UserDraft.Voice(content))
|
||||||
|
setState { copy(sendMode = SendMode.Voice(content)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleSaveTextDraft(draft = composerText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
||||||
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -40,6 +40,7 @@ sealed interface SendMode {
|
|||||||
data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||||
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||||
data class Reply(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
data class Reply(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||||
|
data class Voice(val text: String) : SendMode
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MessageComposerViewState(
|
data class MessageComposerViewState(
|
||||||
@ -47,15 +48,14 @@ data class MessageComposerViewState(
|
|||||||
val canSendMessage: Boolean = true,
|
val canSendMessage: Boolean = true,
|
||||||
val isSendButtonVisible: Boolean = false,
|
val isSendButtonVisible: Boolean = false,
|
||||||
val sendMode: SendMode = SendMode.Regular("", false),
|
val sendMode: SendMode = SendMode.Regular("", false),
|
||||||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.None
|
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
val isVoiceRecording = when (voiceRecordingUiState) {
|
val isVoiceRecording = when (voiceRecordingUiState) {
|
||||||
VoiceMessageRecorderView.RecordingUiState.None,
|
VoiceMessageRecorderView.RecordingUiState.Idle -> false
|
||||||
VoiceMessageRecorderView.RecordingUiState.Cancelled,
|
|
||||||
VoiceMessageRecorderView.RecordingUiState.Playback -> false
|
|
||||||
is VoiceMessageRecorderView.RecordingUiState.Locked,
|
is VoiceMessageRecorderView.RecordingUiState.Locked,
|
||||||
is VoiceMessageRecorderView.RecordingUiState.Started -> true
|
VoiceMessageRecorderView.RecordingUiState.Draft,
|
||||||
|
is VoiceMessageRecorderView.RecordingUiState.Recording -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
val isVoiceMessageIdle = !isVoiceRecording
|
val isVoiceMessageIdle = !isVoiceRecording
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
|||||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@ -52,13 +53,22 @@ class VoiceMessageHelper @Inject constructor(
|
|||||||
private var amplitudeTicker: CountUpTimer? = null
|
private var amplitudeTicker: CountUpTimer? = null
|
||||||
private var playbackTicker: CountUpTimer? = null
|
private var playbackTicker: CountUpTimer? = null
|
||||||
|
|
||||||
fun startRecording() {
|
fun initializeRecorder(attachmentData: ContentAttachmentData) {
|
||||||
|
voiceRecorder.initializeRecord(attachmentData)
|
||||||
|
amplitudeList.clear()
|
||||||
|
attachmentData.waveform?.let {
|
||||||
|
amplitudeList.addAll(it)
|
||||||
|
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startRecording(roomId: String) {
|
||||||
stopPlayback()
|
stopPlayback()
|
||||||
playbackTracker.makeAllPlaybacksIdle()
|
playbackTracker.makeAllPlaybacksIdle()
|
||||||
amplitudeList.clear()
|
amplitudeList.clear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
voiceRecorder.startRecord()
|
voiceRecorder.startRecord(roomId)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Unable to start recording")
|
Timber.e(failure, "Unable to start recording")
|
||||||
throw VoiceFailure.UnableToRecord(failure)
|
throw VoiceFailure.UnableToRecord(failure)
|
||||||
@ -78,7 +88,8 @@ class VoiceMessageHelper @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
voiceMessageFile?.let {
|
voiceMessageFile?.let {
|
||||||
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it, "Voice message.${it.extension}")
|
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it, "Voice message.${it.extension}")
|
||||||
return outputFileUri.toMultiPickerAudioType(context)
|
return outputFileUri
|
||||||
|
.toMultiPickerAudioType(context)
|
||||||
?.apply {
|
?.apply {
|
||||||
waveform = if (amplitudeList.size < 50) {
|
waveform = if (amplitudeList.size < 50) {
|
||||||
amplitudeList
|
amplitudeList
|
||||||
@ -218,12 +229,16 @@ class VoiceMessageHelper @Inject constructor(
|
|||||||
playbackTicker = null
|
playbackTicker = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopAllVoiceActions(deleteRecord: Boolean = true) {
|
fun clearTracker() {
|
||||||
stopRecording()
|
playbackTracker.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? {
|
||||||
|
val audioType = stopRecording()
|
||||||
stopPlayback()
|
stopPlayback()
|
||||||
if (deleteRecord) {
|
if (deleteRecord) {
|
||||||
deleteRecording()
|
deleteRecording()
|
||||||
}
|
}
|
||||||
playbackTracker.clear()
|
return audioType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,14 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onSendVoiceMessage() = callback.onSendVoiceMessage()
|
override fun onSendVoiceMessage() = callback.onSendVoiceMessage()
|
||||||
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
|
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
|
||||||
override fun onWaveformClicked() = callback.onRecordingWaveformClicked()
|
override fun onWaveformClicked() {
|
||||||
|
when (lastKnownState) {
|
||||||
|
RecordingUiState.Draft -> callback.onVoicePlaybackButtonClicked()
|
||||||
|
is RecordingUiState.Recording,
|
||||||
|
is RecordingUiState.Locked -> callback.onRecordingWaveformClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked()
|
override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked()
|
||||||
override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
|
override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
|
||||||
onDrag(dragState, newDragState = nextDragStateCreator(dragState))
|
onDrag(dragState, newDragState = nextDragStateCreator(dragState))
|
||||||
@ -112,19 +119,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||||||
fun render(recordingState: RecordingUiState) {
|
fun render(recordingState: RecordingUiState) {
|
||||||
if (lastKnownState == recordingState) return
|
if (lastKnownState == recordingState) return
|
||||||
when (recordingState) {
|
when (recordingState) {
|
||||||
RecordingUiState.None -> {
|
RecordingUiState.Idle -> {
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
is RecordingUiState.Started -> {
|
is RecordingUiState.Recording -> {
|
||||||
startRecordingTicker(startFromLocked = false, startAt = recordingState.recordingStartTimestamp)
|
startRecordingTicker(startFromLocked = false, startAt = recordingState.recordingStartTimestamp)
|
||||||
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||||
voiceMessageViews.showRecordingViews()
|
voiceMessageViews.showRecordingViews()
|
||||||
dragState = DraggingState.Ready
|
dragState = DraggingState.Ready
|
||||||
}
|
}
|
||||||
RecordingUiState.Cancelled -> {
|
|
||||||
reset()
|
|
||||||
vibrate(context)
|
|
||||||
}
|
|
||||||
is RecordingUiState.Locked -> {
|
is RecordingUiState.Locked -> {
|
||||||
if (lastKnownState == null) {
|
if (lastKnownState == null) {
|
||||||
startRecordingTicker(startFromLocked = true, startAt = recordingState.recordingStartTimestamp)
|
startRecordingTicker(startFromLocked = true, startAt = recordingState.recordingStartTimestamp)
|
||||||
@ -134,9 +137,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||||||
voiceMessageViews.showRecordingLockedViews(recordingState)
|
voiceMessageViews.showRecordingLockedViews(recordingState)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
RecordingUiState.Playback -> {
|
RecordingUiState.Draft -> {
|
||||||
stopRecordingTicker()
|
stopRecordingTicker()
|
||||||
voiceMessageViews.showPlaybackViews()
|
voiceMessageViews.showDraftViews()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastKnownState = recordingState
|
lastKnownState = recordingState
|
||||||
@ -220,11 +223,10 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface RecordingUiState {
|
sealed interface RecordingUiState {
|
||||||
object None : RecordingUiState
|
object Idle : RecordingUiState
|
||||||
data class Started(val recordingStartTimestamp: Long) : RecordingUiState
|
data class Recording(val recordingStartTimestamp: Long) : RecordingUiState
|
||||||
object Cancelled : RecordingUiState
|
|
||||||
data class Locked(val recordingStartTimestamp: Long) : RecordingUiState
|
data class Locked(val recordingStartTimestamp: Long) : RecordingUiState
|
||||||
object Playback : RecordingUiState
|
object Draft : RecordingUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface DraggingState {
|
sealed interface DraggingState {
|
||||||
|
@ -23,9 +23,11 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.doOnLayout
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import com.visualizer.amplitude.AudioRecordView
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.setAttributeBackground
|
import im.vector.app.core.extensions.setAttributeBackground
|
||||||
import im.vector.app.core.extensions.setAttributeTintedBackground
|
import im.vector.app.core.extensions.setAttributeTintedBackground
|
||||||
@ -195,7 +197,7 @@ class VoiceMessageViews(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide toasts if user cancelled recording before the timeout of the toast.
|
// Hide toasts if user cancelled recording before the timeout of the toast.
|
||||||
if (recordingState == RecordingUiState.Cancelled || recordingState == RecordingUiState.None) {
|
if (recordingState == RecordingUiState.Idle) {
|
||||||
hideToast()
|
hideToast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,6 +260,16 @@ class VoiceMessageViews(
|
|||||||
views.voiceMessageToast.isVisible = false
|
views.voiceMessageToast.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showDraftViews() {
|
||||||
|
hideRecordingViews(RecordingUiState.Idle)
|
||||||
|
views.voiceMessageMicButton.isVisible = false
|
||||||
|
views.voiceMessageSendButton.isVisible = true
|
||||||
|
views.voiceMessagePlaybackLayout.isVisible = true
|
||||||
|
views.voiceMessagePlaybackTimerIndicator.isVisible = false
|
||||||
|
views.voicePlaybackControlButton.isVisible = true
|
||||||
|
views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
}
|
||||||
|
|
||||||
fun showRecordingLockedViews(recordingState: RecordingUiState) {
|
fun showRecordingLockedViews(recordingState: RecordingUiState) {
|
||||||
hideRecordingViews(recordingState)
|
hideRecordingViews(recordingState)
|
||||||
views.voiceMessagePlaybackLayout.isVisible = true
|
views.voiceMessagePlaybackLayout.isVisible = true
|
||||||
@ -268,14 +280,8 @@ class VoiceMessageViews(
|
|||||||
renderToast(resources.getString(R.string.voice_message_tap_to_stop_toast))
|
renderToast(resources.getString(R.string.voice_message_tap_to_stop_toast))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showPlaybackViews() {
|
|
||||||
views.voiceMessagePlaybackTimerIndicator.isVisible = false
|
|
||||||
views.voicePlaybackControlButton.isVisible = true
|
|
||||||
views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initViews() {
|
fun initViews() {
|
||||||
hideRecordingViews(RecordingUiState.None)
|
hideRecordingViews(RecordingUiState.Idle)
|
||||||
views.voiceMessageMicButton.isVisible = true
|
views.voiceMessageMicButton.isVisible = true
|
||||||
views.voiceMessageSendButton.isVisible = false
|
views.voiceMessageSendButton.isVisible = false
|
||||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
||||||
@ -320,11 +326,9 @@ class VoiceMessageViews(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun renderRecordingWaveform(amplitudeList: Array<Int>) {
|
fun renderRecordingWaveform(amplitudeList: Array<Int>) {
|
||||||
views.voicePlaybackWaveform.post {
|
views.voicePlaybackWaveform.doOnLayout { waveFormView ->
|
||||||
views.voicePlaybackWaveform.apply {
|
|
||||||
amplitudeList.iterator().forEach {
|
amplitudeList.iterator().forEach {
|
||||||
update(it)
|
(waveFormView as AudioRecordView).update(it)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import android.media.MediaRecorder
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
|
import org.matrix.android.sdk.internal.util.md5
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -60,9 +61,17 @@ abstract class AbstractVoiceRecorder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startRecord() {
|
override fun initializeRecord(attachmentData: ContentAttachmentData) {
|
||||||
|
outputFile = attachmentData.findVoiceFile(outputDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startRecord(roomId: String) {
|
||||||
init()
|
init()
|
||||||
outputFile = File(outputDirectory, "${UUID.randomUUID()}.$filenameExt")
|
val fileName = "${UUID.randomUUID()}.$filenameExt"
|
||||||
|
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
outputFile = File(outputDirectoryForRoom, fileName)
|
||||||
|
|
||||||
val mr = mediaRecorder ?: return
|
val mr = mediaRecorder ?: return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@ -104,7 +113,6 @@ abstract class AbstractVoiceRecorder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED") // preemptively added for https://github.com/vector-im/element-android/pull/4527
|
|
||||||
private fun ContentAttachmentData.findVoiceFile(baseDirectory: File): File {
|
private fun ContentAttachmentData.findVoiceFile(baseDirectory: File): File {
|
||||||
return File(baseDirectory, queryUri.takePathAfter(baseDirectory.name))
|
return File(baseDirectory, queryUri.takePathAfter(baseDirectory.name))
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,21 @@
|
|||||||
|
|
||||||
package im.vector.app.features.voice
|
package im.vector.app.features.voice
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface VoiceRecorder {
|
interface VoiceRecorder {
|
||||||
/**
|
/**
|
||||||
* Start the recording
|
* Initialize recording with a pre-recorded file.
|
||||||
|
* @param attachmentData data of the recorded file
|
||||||
*/
|
*/
|
||||||
fun startRecord()
|
fun initializeRecord(attachmentData: ContentAttachmentData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the recording
|
||||||
|
* @param roomId id of the room to start record
|
||||||
|
*/
|
||||||
|
fun startRecord(roomId: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the recording
|
* Stop the recording
|
||||||
|
Loading…
x
Reference in New Issue
Block a user