mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Merge pull request #5586 from vector-im/feature/eric/audio-files-player
Audio files in the timeline now appear with the audio player
This commit is contained in:
commit
45104f8cec
19 changed files with 576 additions and 190 deletions
1
changelog.d/5586.feature
Normal file
1
changelog.d/5586.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Adds the ability for audio attachments to be played in the timeline
|
|
@ -38,7 +38,7 @@ import im.vector.app.databinding.ActivityRoomDetailBinding
|
|||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.room.RequireActiveMembershipAction
|
||||
|
@ -75,7 +75,7 @@ class RoomDetailActivity :
|
|||
}
|
||||
|
||||
private var lastKnownPlayingOrRecordingState: Boolean? = null
|
||||
private val playbackActivityListener = VoiceMessagePlaybackTracker.ActivityListener { isPlayingOrRecording ->
|
||||
private val playbackActivityListener = AudioMessagePlaybackTracker.ActivityListener { isPlayingOrRecording ->
|
||||
if (lastKnownPlayingOrRecordingState == isPlayingOrRecording) return@ActivityListener
|
||||
when (isPlayingOrRecording) {
|
||||
true -> keepScreenOn()
|
||||
|
@ -86,7 +86,7 @@ class RoomDetailActivity :
|
|||
|
||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
@Inject lateinit var playbackTracker: VoiceMessagePlaybackTracker
|
||||
@Inject lateinit var playbackTracker: AudioMessagePlaybackTracker
|
||||
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
|
||||
private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel()
|
||||
|
||||
|
@ -152,7 +152,7 @@ class RoomDetailActivity :
|
|||
override fun onDestroy() {
|
||||
supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
|
||||
views.drawerLayout.removeDrawerListener(drawerListener)
|
||||
playbackTracker.unTrackActivity(playbackActivityListener)
|
||||
playbackTracker.untrackActivity(playbackActivityListener)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
|
@ -156,10 +156,11 @@ import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
|
|||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
@ -264,7 +265,7 @@ class TimelineFragment @Inject constructor(
|
|||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
|
||||
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
|
||||
private val clock: Clock,
|
||||
private val matrixConfiguration: MatrixConfiguration
|
||||
) :
|
||||
|
@ -737,7 +738,7 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun setupVoiceMessageView() {
|
||||
voiceMessagePlaybackTracker.track(VoiceMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
|
||||
audioMessagePlaybackTracker.track(AudioMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
|
||||
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
|
||||
|
||||
override fun onVoiceRecordingStarted() {
|
||||
|
@ -794,13 +795,13 @@ class TimelineFragment @Inject constructor(
|
|||
|
||||
override fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int) {
|
||||
messageComposerViewModel.handle(
|
||||
MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onVoiceWaveformMoved(percentage: Float, duration: Int) {
|
||||
messageComposerViewModel.handle(
|
||||
MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||
MessageComposerAction.VoiceWaveformTouchedUp(AudioMessagePlaybackTracker.RECORDING_ID, duration, percentage)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -908,6 +909,7 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
audioMessagePlaybackTracker.makeAllPlaybacksIdle()
|
||||
lazyLoadedViews.unBind()
|
||||
timelineEventController.callback = null
|
||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||
|
@ -1202,13 +1204,10 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
|
||||
val messageContent: MessageContent? = event.getLastMessageContent()
|
||||
val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) {
|
||||
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||
} else if (messageContent is MessagePollContent) {
|
||||
messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
} else {
|
||||
messageContent?.body ?: ""
|
||||
val nonFormattedBody = when (messageContent) {
|
||||
is MessageAudioContent -> getAudioContentBodyText(messageContent)
|
||||
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
else -> messageContent?.body.orEmpty()
|
||||
}
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||
|
@ -1247,6 +1246,15 @@ class TimelineFragment @Inject constructor(
|
|||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
|
||||
private fun getAudioContentBodyText(messageContent: MessageAudioContent): String {
|
||||
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||
return if (messageContent.voiceMessageIndicator != null) {
|
||||
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||
} else {
|
||||
getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
notificationDrawerManager.setCurrentRoom(timelineArgs.roomId)
|
||||
|
@ -1271,7 +1279,7 @@ class TimelineFragment @Inject constructor(
|
|||
override fun onPause() {
|
||||
super.onPause()
|
||||
notificationDrawerManager.setCurrentRoom(null)
|
||||
voiceMessagePlaybackTracker.unTrack(VoiceMessagePlaybackTracker.RECORDING_ID)
|
||||
audioMessagePlaybackTracker.pauseAllPlaybacks()
|
||||
|
||||
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||
// we're rotating, maintain any active recordings
|
||||
|
@ -1399,6 +1407,7 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
return when (model) {
|
||||
is MessageFileItem,
|
||||
is MessageAudioItem,
|
||||
is MessageVoiceItem,
|
||||
is MessageImageVideoItem,
|
||||
is MessageTextItem -> {
|
||||
|
@ -2088,6 +2097,10 @@ class TimelineFragment @Inject constructor(
|
|||
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
|
||||
}
|
||||
|
||||
override fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float) {
|
||||
messageComposerViewModel.handle(MessageComposerAction.AudioSeekBarMovedTo(eventId, duration, percentage))
|
||||
}
|
||||
|
||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||
when (action.messageContent) {
|
||||
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
|
||||
|
|
|
@ -21,7 +21,7 @@ import android.media.AudioAttributes
|
|||
import android.media.MediaPlayer
|
||||
import androidx.core.content.FileProvider
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voice.VoiceRecorder
|
||||
import im.vector.app.features.voice.VoiceRecorderProvider
|
||||
|
@ -40,12 +40,13 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Helper class to record audio for voice messages.
|
||||
*/
|
||||
class VoiceMessageHelper @Inject constructor(
|
||||
class AudioMessageHelper @Inject constructor(
|
||||
private val context: Context,
|
||||
private val playbackTracker: VoiceMessagePlaybackTracker,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
voiceRecorderProvider: VoiceRecorderProvider
|
||||
) {
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var currentPlayingId: String? = null
|
||||
private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder()
|
||||
|
||||
private val amplitudeList = mutableListOf<Int>()
|
||||
|
@ -58,7 +59,7 @@ class VoiceMessageHelper @Inject constructor(
|
|||
amplitudeList.clear()
|
||||
attachmentData.waveform?.let {
|
||||
amplitudeList.addAll(it)
|
||||
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
playbackTracker.updateCurrentRecording(AudioMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +128,7 @@ class VoiceMessageHelper @Inject constructor(
|
|||
|
||||
fun startOrPauseRecordingPlayback() {
|
||||
voiceRecorder.getCurrentRecord()?.let {
|
||||
startOrPausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID, it)
|
||||
startOrPausePlayback(AudioMessagePlaybackTracker.RECORDING_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +137,8 @@ class VoiceMessageHelper @Inject constructor(
|
|||
mediaPlayer?.stop()
|
||||
stopPlaybackTicker()
|
||||
stopRecordingAmplitudes()
|
||||
if (playbackState is VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
currentPlayingId = null
|
||||
if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) {
|
||||
playbackTracker.pausePlayback(id)
|
||||
} else {
|
||||
startPlayback(id, file)
|
||||
|
@ -163,6 +165,7 @@ class VoiceMessageHelper @Inject constructor(
|
|||
seekTo(currentPlaybackTime)
|
||||
}
|
||||
}
|
||||
currentPlayingId = id
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Unable to start playback")
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
|
@ -171,17 +174,24 @@ class VoiceMessageHelper @Inject constructor(
|
|||
}
|
||||
|
||||
fun stopPlayback() {
|
||||
playbackTracker.pausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
|
||||
playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID)
|
||||
mediaPlayer?.stop()
|
||||
stopPlaybackTicker()
|
||||
currentPlayingId = null
|
||||
}
|
||||
|
||||
fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
|
||||
val toMillisecond = (totalDuration * percentage).toInt()
|
||||
playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)
|
||||
playbackTracker.pauseAllPlaybacks()
|
||||
|
||||
stopPlayback()
|
||||
playbackTracker.pausePlayback(id)
|
||||
if (currentPlayingId == id) {
|
||||
mediaPlayer?.seekTo(toMillisecond)
|
||||
playbackTracker.updatePlayingAtPlaybackTime(id, toMillisecond, percentage)
|
||||
} else {
|
||||
mediaPlayer?.pause()
|
||||
playbackTracker.updatePausedAtPlaybackTime(id, toMillisecond, percentage)
|
||||
stopPlaybackTicker()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRecordingAmplitudes() {
|
||||
|
@ -200,7 +210,7 @@ class VoiceMessageHelper @Inject constructor(
|
|||
try {
|
||||
val maxAmplitude = voiceRecorder.getMaxAmplitude()
|
||||
amplitudeList.add(maxAmplitude)
|
||||
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
playbackTracker.updateCurrentRecording(AudioMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.e(e, "Cannot get max amplitude. Amplitude recording timer will be stopped.")
|
||||
stopRecordingAmplitudes()
|
||||
|
@ -233,7 +243,7 @@ class VoiceMessageHelper @Inject constructor(
|
|||
val currentPosition = mediaPlayer?.currentPosition ?: 0
|
||||
val totalDuration = mediaPlayer?.duration ?: 0
|
||||
val percentage = currentPosition.toFloat() / totalDuration
|
||||
playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage)
|
||||
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
|
||||
} else {
|
||||
playbackTracker.stopPlayback(id)
|
||||
stopPlaybackTicker()
|
|
@ -42,4 +42,5 @@ sealed class MessageComposerAction : VectorViewModelAction {
|
|||
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
|
||||
data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||
data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||
data class AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
private val vectorPreferences: VectorPreferences,
|
||||
private val commandParser: CommandParser,
|
||||
private val rainbowGenerator: RainbowGenerator,
|
||||
private val voiceMessageHelper: VoiceMessageHelper,
|
||||
private val audioMessageHelper: AudioMessageHelper,
|
||||
private val analyticsTracker: AnalyticsTracker,
|
||||
private val voicePlayerHelper: VoicePlayerHelper
|
||||
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
||||
|
@ -90,7 +90,6 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun handle(action: MessageComposerAction) {
|
||||
Timber.v("Handle action: $action")
|
||||
when (action) {
|
||||
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
||||
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||
|
@ -110,6 +109,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
|
||||
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
|
||||
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
|
||||
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -811,18 +811,18 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
|
||||
private fun handleStartRecordingVoiceMessage() {
|
||||
try {
|
||||
voiceMessageHelper.startRecording(room.roomId)
|
||||
audioMessageHelper.startRecording(room.roomId)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
|
||||
voiceMessageHelper.stopPlayback()
|
||||
audioMessageHelper.stopPlayback()
|
||||
if (isCancelled) {
|
||||
voiceMessageHelper.deleteRecording()
|
||||
audioMessageHelper.deleteRecording()
|
||||
} else {
|
||||
voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
|
||||
audioMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
|
||||
if (audioType.duration > 1000) {
|
||||
room.sendMedia(
|
||||
attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
|
||||
|
@ -830,7 +830,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
roomIds = emptySet(),
|
||||
rootThreadEventId = rootThreadEventId)
|
||||
} else {
|
||||
voiceMessageHelper.deleteRecording()
|
||||
audioMessageHelper.deleteRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -845,7 +845,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
||||
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
||||
// Play can fail
|
||||
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
||||
audioMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||
}
|
||||
|
@ -853,35 +853,38 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handlePlayOrPauseRecordingPlayback() {
|
||||
voiceMessageHelper.startOrPauseRecordingPlayback()
|
||||
audioMessageHelper.startOrPauseRecordingPlayback()
|
||||
}
|
||||
|
||||
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
||||
voiceMessageHelper.clearTracker()
|
||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||
audioMessageHelper.clearTracker()
|
||||
audioMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||
}
|
||||
|
||||
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
||||
voiceMessageHelper.initializeRecorder(attachmentData)
|
||||
audioMessageHelper.initializeRecorder(attachmentData)
|
||||
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
||||
}
|
||||
|
||||
private fun handlePauseRecordingVoiceMessage() {
|
||||
voiceMessageHelper.pauseRecording()
|
||||
audioMessageHelper.pauseRecording()
|
||||
}
|
||||
|
||||
private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
|
||||
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||
}
|
||||
|
||||
private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
|
||||
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||
}
|
||||
|
||||
private fun handleAudioSeekBarMovedTo(action: MessageComposerAction.AudioSeekBarMovedTo) {
|
||||
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||
}
|
||||
|
||||
private fun handleEntersBackground(composerText: String) {
|
||||
// Always stop all voice actions. It may be playing in timeline or active recording
|
||||
val playingAudioContent = voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
||||
voiceMessageHelper.clearTracker()
|
||||
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
||||
|
||||
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
||||
if (isVoiceRecording) {
|
||||
|
|
|
@ -27,7 +27,7 @@ import im.vector.app.core.hardware.vibrate
|
|||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.floor
|
||||
|
@ -40,7 +40,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener {
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), AudioMessagePlaybackTracker.Listener {
|
||||
|
||||
interface Callback {
|
||||
fun onVoiceRecordingStarted()
|
||||
|
@ -222,16 +222,16 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
recordingTicker = null
|
||||
}
|
||||
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Recording -> {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> {
|
||||
voiceMessageViews.renderRecordingWaveform(state.amplitudeList.toTypedArray())
|
||||
}
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> {
|
||||
voiceMessageViews.renderPlaying(state)
|
||||
}
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused,
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused,
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
|
||||
voiceMessageViews.renderIdle()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
|
||||
|
@ -303,7 +303,7 @@ class VoiceMessageViews(
|
|||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.clear() }
|
||||
}
|
||||
|
||||
fun renderPlaying(state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
fun renderPlaying(state: AudioMessagePlaybackTracker.Listener.State.Playing) {
|
||||
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message)
|
||||
val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong())
|
||||
|
|
|
@ -148,6 +148,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
|
||||
fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
|
||||
|
||||
fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float)
|
||||
|
||||
fun onAddMoreReaction(event: TimelineEvent)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.app.core.resources.StringProvider
|
|||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.containsOnlyEmojis
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
|
@ -41,8 +42,9 @@ import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvid
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
|
@ -109,7 +111,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
|
@ -140,7 +141,7 @@ class MessageItemFactory @Inject constructor(
|
|||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val spanUtils: SpanUtils,
|
||||
private val session: Session,
|
||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
|
||||
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
|
||||
private val locationPinProvider: LocationPinProvider,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val urlMapProvider: UrlMapProvider,
|
||||
|
@ -201,13 +202,7 @@ class MessageItemFactory @Inject constructor(
|
|||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||
is MessageAudioContent -> {
|
||||
if (messageContent.voiceMessageIndicator != null) {
|
||||
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
|
||||
} else {
|
||||
buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||
}
|
||||
}
|
||||
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageLocationContent -> {
|
||||
|
@ -279,9 +274,9 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createPollState(
|
||||
informationData: MessageInformationData,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
pollContent: MessagePollContent,
|
||||
informationData: MessageInformationData,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
pollContent: MessagePollContent,
|
||||
): PollState = when {
|
||||
!informationData.sendState.isSent() -> Sending
|
||||
pollResponseSummary?.isClosed.orFalse() -> Ended
|
||||
|
@ -291,8 +286,8 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun List<PollAnswer>.mapToOptions(
|
||||
pollState: PollState,
|
||||
informationData: MessageInformationData,
|
||||
pollState: PollState,
|
||||
informationData: MessageInformationData,
|
||||
) = map { answer ->
|
||||
val pollResponseSummary = informationData.pollResponseAggregatedSummary
|
||||
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
|
||||
|
@ -314,9 +309,9 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createPollQuestion(
|
||||
informationData: MessageInformationData,
|
||||
question: String,
|
||||
callback: TimelineEventController.Callback?,
|
||||
informationData: MessageInformationData,
|
||||
question: String,
|
||||
callback: TimelineEventController.Callback?,
|
||||
) = if (informationData.hasBeenEdited) {
|
||||
annotateWithEdited(question, callback, informationData)
|
||||
} else {
|
||||
|
@ -324,8 +319,8 @@ class MessageItemFactory @Inject constructor(
|
|||
}.toEpoxyCharSequence()
|
||||
|
||||
private fun createTotalVotesText(
|
||||
pollState: PollState,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
pollState: PollState,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
): String {
|
||||
val votes = pollResponseSummary?.totalVotes ?: 0
|
||||
return when {
|
||||
|
@ -338,58 +333,62 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun buildAudioMessageItem(
|
||||
messageContent: MessageAudioContent,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): MessageFileItem? {
|
||||
val fileUrl = messageContent.getFileUrl()?.let {
|
||||
if (informationData.sentByMe && !informationData.sendState.isSent()) {
|
||||
it
|
||||
} else {
|
||||
it.takeIf { it.isMxcUrl() }
|
||||
}
|
||||
} ?: ""
|
||||
return MessageFileItem_()
|
||||
.attributes(attributes)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
fileUrl,
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.mxcUrl(fileUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.filename(messageContent.body)
|
||||
.iconRes(R.drawable.ic_headphones)
|
||||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes
|
||||
): MessageAudioItem {
|
||||
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
||||
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
||||
val duration = messageContent.audioInfo?.duration ?: 0
|
||||
|
||||
return MessageAudioItem_()
|
||||
.attributes(attributes)
|
||||
.filename(messageContent.body)
|
||||
.duration(messageContent.audioInfo?.duration ?: 0)
|
||||
.playbackControlButtonClickListener(playbackControlButtonClickListener)
|
||||
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
||||
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||
.fileSize(messageContent.audioInfo?.size ?: 0L)
|
||||
.onSeek { params.callback?.onAudioSeekBarMovedTo(informationData.eventId, duration, it) }
|
||||
.mxcUrl(fileUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
}
|
||||
|
||||
private fun getAudioFileUrl(
|
||||
messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
) = messageContent.getFileUrl()?.let {
|
||||
if (informationData.sentByMe && !informationData.sendState.isSent()) {
|
||||
it
|
||||
} else {
|
||||
it.takeIf { it.isMxcUrl() }
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
private fun createOnPlaybackButtonClickListener(
|
||||
messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
params: TimelineItemFactoryParams,
|
||||
) = object : ClickListener {
|
||||
override fun invoke(view: View) {
|
||||
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildVoiceMessageItem(
|
||||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): MessageVoiceItem? {
|
||||
val fileUrl = messageContent.getFileUrl()?.let {
|
||||
if (informationData.sentByMe && !informationData.sendState.isSent()) {
|
||||
it
|
||||
} else {
|
||||
it.takeIf { it.isMxcUrl() }
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
val playbackControlButtonClickListener: ClickListener = object : ClickListener {
|
||||
override fun invoke(view: View) {
|
||||
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
|
||||
}
|
||||
}
|
||||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes
|
||||
): MessageVoiceItem {
|
||||
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
||||
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
||||
|
||||
val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener {
|
||||
override fun onWaveformTouchedUp(percentage: Float) {
|
||||
|
@ -409,14 +408,8 @@ class MessageItemFactory @Inject constructor(
|
|||
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
|
||||
.playbackControlButtonClickListener(playbackControlButtonClickListener)
|
||||
.waveformTouchListener(waveformTouchListener)
|
||||
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
fileUrl,
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
||||
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||
.mxcUrl(fileUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
|
@ -470,13 +463,13 @@ class MessageItemFactory @Inject constructor(
|
|||
messageContent: MessageFileContent,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): MessageFileItem? {
|
||||
): MessageFileItem {
|
||||
val mxcUrl = messageContent.getFileUrl() ?: ""
|
||||
return MessageFileItem_()
|
||||
.attributes(attributes)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
|
||||
.izDownloaded(session.fileService().isFileInCache(messageContent))
|
||||
.isLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
|
||||
.isDownloaded(session.fileService().isFileInCache(messageContent))
|
||||
.mxcUrl(mxcUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
|
@ -485,13 +478,24 @@ class MessageItemFactory @Inject constructor(
|
|||
.iconRes(R.drawable.ic_paperclip)
|
||||
}
|
||||
|
||||
private fun buildAudioContent(
|
||||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
) = if (messageContent.voiceMessageIndicator != null) {
|
||||
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
|
||||
} else {
|
||||
buildAudioMessageItem(params, messageContent, informationData, highlight, attributes)
|
||||
}
|
||||
|
||||
private fun buildNotHandledMessageItem(
|
||||
messageContent: MessageContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): MessageTextItem? {
|
||||
messageContent: MessageContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
// For compatibility reason we should display the body
|
||||
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VoiceMessagePlaybackTracker @Inject constructor() {
|
||||
class AudioMessagePlaybackTracker @Inject constructor() {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val listeners = mutableMapOf<String, Listener>()
|
||||
|
@ -33,7 +33,7 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
activityListeners.add(listener)
|
||||
}
|
||||
|
||||
fun unTrackActivity(listener: ActivityListener) {
|
||||
fun untrackActivity(listener: ActivityListener) {
|
||||
activityListeners.remove(listener)
|
||||
}
|
||||
|
||||
|
@ -46,10 +46,16 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
fun unTrack(id: String) {
|
||||
fun untrack(id: String) {
|
||||
listeners.remove(id)
|
||||
}
|
||||
|
||||
fun pauseAllPlaybacks() {
|
||||
listeners.keys.forEach { key ->
|
||||
pausePlayback(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun makeAllPlaybacksIdle() {
|
||||
listeners.keys.forEach { key ->
|
||||
setState(key, Listener.State.Idle)
|
||||
|
@ -87,19 +93,25 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
|
||||
fun pausePlayback(id: String) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
val currentPercentage = getPercentage(id)
|
||||
setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
|
||||
if (getPlaybackState(id) is Listener.State.Playing) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
val currentPercentage = getPercentage(id)
|
||||
setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
|
||||
}
|
||||
}
|
||||
|
||||
fun stopPlayback(id: String) {
|
||||
setState(id, Listener.State.Idle)
|
||||
}
|
||||
|
||||
fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||
setState(id, Listener.State.Playing(time, percentage))
|
||||
}
|
||||
|
||||
fun updatePausedAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||
setState(id, Listener.State.Paused(time, percentage))
|
||||
}
|
||||
|
||||
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
|
||||
setState(id, Listener.State.Recording(amplitudeList))
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.text.format.DateUtils
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var filename: String = ""
|
||||
|
||||
@EpoxyAttribute
|
||||
var mxcUrl: String = ""
|
||||
|
||||
@EpoxyAttribute
|
||||
var duration: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var fileSize: Long = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
@JvmField
|
||||
var isLocalFile = false
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var onSeek: ((percentage: Float) -> Unit)? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var playbackControlButtonClickListener: ClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
||||
|
||||
private var isUserSeeking = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.rootLayout, null)
|
||||
bindViewAttributes(holder)
|
||||
bindUploadState(holder)
|
||||
applyLayoutTint(holder)
|
||||
bindSeekBar(holder)
|
||||
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
renderStateBasedOnAudioPlayback(holder)
|
||||
}
|
||||
|
||||
private fun bindUploadState(holder: Holder) {
|
||||
if (attributes.informationData.sendState.hasFailed()) {
|
||||
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_cross)
|
||||
holder.audioPlaybackControlButton.contentDescription =
|
||||
holder.view.context.getString(R.string.error_audio_message_unable_to_play, filename)
|
||||
holder.progressLayout.isVisible = false
|
||||
} else {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyLayoutTint(holder: Holder) {
|
||||
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
|
||||
Color.TRANSPARENT
|
||||
} else {
|
||||
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
|
||||
}
|
||||
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||
}
|
||||
|
||||
private fun bindViewAttributes(holder: Holder) {
|
||||
val formattedDuration = formatPlaybackTime(duration)
|
||||
val formattedFileSize = TextUtils.formatFileSize(holder.rootLayout.context, fileSize, true)
|
||||
val durationContentDescription = getPlaybackTimeContentDescription(holder.rootLayout.context, duration)
|
||||
|
||||
holder.filenameView.text = filename
|
||||
holder.filenameView.onClick(attributes.itemClickListener)
|
||||
holder.audioPlaybackDuration.text = formattedDuration
|
||||
holder.fileSize.text = holder.rootLayout.context.getString(
|
||||
R.string.audio_message_file_size, formattedFileSize
|
||||
)
|
||||
holder.mainLayout.contentDescription = holder.rootLayout.context.getString(
|
||||
R.string.a11y_audio_message_item, filename, durationContentDescription, formattedFileSize
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindSeekBar(holder: Holder) {
|
||||
holder.audioSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
holder.audioPlaybackTime.text = formatPlaybackTime(
|
||||
(duration * (progress.toFloat() / 100)).toInt()
|
||||
)
|
||||
}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
isUserSeeking = false
|
||||
val percentage = seekBar.progress.toFloat() / 100
|
||||
onSeek?.invoke(percentage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun renderIdleState(holder: Holder) {
|
||||
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.audioPlaybackControlButton.contentDescription =
|
||||
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
||||
holder.audioPlaybackTime.text = formatPlaybackTime(duration)
|
||||
holder.audioSeekBar.progress = 0
|
||||
}
|
||||
|
||||
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
|
||||
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
holder.audioPlaybackControlButton.contentDescription =
|
||||
holder.view.context.getString(R.string.a11y_pause_audio_message, filename)
|
||||
|
||||
if (!isUserSeeking) {
|
||||
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
|
||||
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.audioPlaybackControlButton.contentDescription =
|
||||
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
||||
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
|
||||
}
|
||||
|
||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||
|
||||
private fun getPlaybackTimeContentDescription(context: Context, time: Int): String {
|
||||
val formattedPlaybackTime = formatPlaybackTime(time)
|
||||
val (minutes, seconds) = formattedPlaybackTime.split(":").map { it.toIntOrNull() ?: 0 }
|
||||
return context.getString(R.string.a11y_audio_playback_duration, minutes, seconds)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
|
||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
audioMessagePlaybackTracker.untrack(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val rootLayout by bind<ViewGroup>(R.id.messageRootLayout)
|
||||
val mainLayout by bind<ViewGroup>(R.id.messageMainInnerLayout)
|
||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||
val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton)
|
||||
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||
val fileSize by bind<TextView>(R.id.fileSize)
|
||||
val audioPlaybackDuration by bind<TextView>(R.id.audioPlaybackDuration)
|
||||
val audioSeekBar by bind<SeekBar>(R.id.audioSeekBar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentAudioStub
|
||||
}
|
||||
}
|
|
@ -47,14 +47,13 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
@DrawableRes
|
||||
var iconRes: Int = 0
|
||||
|
||||
// @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
// var clickListener: ClickListener? = null
|
||||
@EpoxyAttribute
|
||||
@JvmField
|
||||
var isLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var izLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var izDownloaded = false
|
||||
@JvmField
|
||||
var isDownloaded = false
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
@ -65,17 +64,20 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.fileLayout, holder.filenameView)
|
||||
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
|
||||
} else {
|
||||
holder.fileImageView.setImageResource(R.drawable.ic_cross)
|
||||
holder.progressLayout.isVisible = false
|
||||
}
|
||||
|
||||
holder.filenameView.text = filename
|
||||
|
||||
if (attributes.informationData.sendState.isSending()) {
|
||||
holder.fileImageView.setImageResource(iconRes)
|
||||
} else {
|
||||
if (izDownloaded) {
|
||||
if (isDownloaded) {
|
||||
holder.fileImageView.setImageResource(iconRes)
|
||||
holder.fileDownloadProgress.progress = 0
|
||||
} else {
|
||||
|
@ -83,7 +85,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
holder.fileImageView.setImageResource(R.drawable.ic_download)
|
||||
}
|
||||
}
|
||||
// holder.view.setOnClickListener(clickListener)
|
||||
|
||||
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
|
||||
Color.TRANSPARENT
|
||||
} else {
|
||||
|
|
|
@ -30,9 +30,9 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
|
@ -55,10 +55,8 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
var waveform: List<Int> = emptyList()
|
||||
|
||||
@EpoxyAttribute
|
||||
var izLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var izDownloaded = false
|
||||
@JvmField
|
||||
var isLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
@ -73,13 +71,13 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
var waveformTouchListener: WaveformTouchListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
|
||||
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.voiceLayout, null)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
|
||||
} else {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.error_voice_message_unable_to_play)
|
||||
|
@ -96,11 +94,11 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
|
||||
}
|
||||
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
}
|
||||
|
||||
private fun onWaveformViewReady(holder: Holder) {
|
||||
holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
|
||||
val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary)
|
||||
val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary)
|
||||
|
@ -125,13 +123,13 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
true
|
||||
}
|
||||
|
||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -146,14 +144,14 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
holder.voicePlaybackWaveform.updateColors(0f, playedColor, idleColor)
|
||||
}
|
||||
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) {
|
||||
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor)
|
||||
}
|
||||
|
||||
private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) {
|
||||
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
|
@ -166,7 +164,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
super.unbind(holder)
|
||||
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
|
||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
|
||||
audioMessagePlaybackTracker.untrack(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
|
21
vector/src/main/res/drawable/bg_seek_bar.xml
Normal file
21
vector/src/main/res/drawable/bg_seek_bar.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape
|
||||
android:shape="line">
|
||||
<stroke
|
||||
android:color="?vctr_content_quaternary"
|
||||
android:width="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape
|
||||
android:shape="line">
|
||||
<stroke
|
||||
android:color="?vctr_content_tertiary"
|
||||
android:width="2dp"/>
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -1,16 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,18V12C3,7.0294 7.0294,3 12,3C16.9706,3 21,7.0294 21,12V18"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M21,14H22C22,13.4477 21.5523,13 21,13V14ZM3,14V13C2.4477,13 2,13.4477 2,14H3ZM20,19C20,19.5523 19.5523,20 19,20V22C20.6569,22 22,20.6569 22,19H20ZM19,20H18V22H19V20ZM18,20C17.4477,20 17,19.5523 17,19H15C15,20.6569 16.3431,22 18,22V20ZM17,19V16H15V19H17ZM17,16C17,15.4477 17.4477,15 18,15V13C16.3431,13 15,14.3431 15,16H17ZM18,15H21V13H18V15ZM20,14V19H22V14H20ZM2,19C2,20.6569 3.3431,22 5,22V20C4.4477,20 4,19.5523 4,19H2ZM5,22H6V20H5V22ZM6,22C7.6568,22 9,20.6569 9,19H7C7,19.5523 6.5523,20 6,20V22ZM9,19V16H7V19H9ZM9,16C9,14.3431 7.6568,13 6,13V15C6.5523,15 7,15.4477 7,16H9ZM6,13H3V15H6V13ZM2,14V19H4V14H2Z"
|
||||
android:fillColor="#2E2F32"/>
|
||||
</vector>
|
107
vector/src/main/res/layout/item_timeline_event_audio_stub.xml
Normal file
107
vector/src/main/res/layout/item_timeline_event_audio_stub.xml
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/messageRootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/messageMainInnerLayout"
|
||||
style="@style/TimelineContentMediaPillStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/audioPlaybackControlButton"
|
||||
android:layout_width="@dimen/item_event_message_media_button_size"
|
||||
android:layout_height="@dimen/item_event_message_media_button_size"
|
||||
android:background="@drawable/bg_voice_play_pause_button"
|
||||
android:backgroundTint="?android:colorBackground"
|
||||
android:contentDescription="@string/a11y_play_voice_message"
|
||||
android:src="@drawable/ic_play_pause_play"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageFilenameView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:autoLink="none"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginTop="6dp"
|
||||
app:layout_constraintStart_toEndOf="@id/audioPlaybackControlButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Filename.mp3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/audioPlaybackDuration"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?vctr_content_tertiary"
|
||||
app:layout_constraintStart_toStartOf="@id/messageFilenameView"
|
||||
app:layout_constraintTop_toBottomOf="@id/messageFilenameView"
|
||||
tools:text="0:23" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fileSize"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?vctr_content_tertiary"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintStart_toEndOf="@id/audioPlaybackDuration"
|
||||
app:layout_constraintBottom_toBottomOf="@id/audioPlaybackDuration"
|
||||
tools:text="(2MB)" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/audioSeekBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:progressDrawable="@drawable/bg_seek_bar"
|
||||
android:thumbTint="?vctr_content_tertiary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/audioPlaybackTime"
|
||||
app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:progress="40" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/audioPlaybackTime"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?vctr_content_tertiary"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/audioSeekBar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/audioSeekBar"
|
||||
tools:text="0:23" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/messageFileUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
|
@ -40,6 +40,13 @@
|
|||
android:layout="@layout/item_timeline_event_voice_stub"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentAudioStub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_audio_stub"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentPollStub"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -2892,6 +2892,14 @@
|
|||
<string name="error_voice_message_cannot_reply_or_edit">Cannot reply or edit while voice message is active</string>
|
||||
<string name="voice_message_reply_content">Voice Message (%1$s)</string>
|
||||
|
||||
<string name="a11y_audio_message_item">%1$s, %2$s, %3$s</string> <!-- filename, duration, file size -->
|
||||
<string name="a11y_audio_playback_duration">%1$d minutes %2$d seconds</string>
|
||||
<string name="a11y_play_audio_message">Play %1$s</string>
|
||||
<string name="a11y_pause_audio_message">Pause %1$s</string>
|
||||
<string name="error_audio_message_unable_to_play">Unable to play %1$s</string>
|
||||
<string name="audio_message_reply_content">%1$s (%2$s)</string>
|
||||
<string name="audio_message_file_size">(%1$s)</string>
|
||||
|
||||
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue