Skip to content

Commit 972d7e7

Browse files
committed
voice messages
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
1 parent cc5e3b7 commit 972d7e7

8 files changed

Lines changed: 307 additions & 46 deletions

File tree

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
140140
import com.nextcloud.talk.api.NcApi
141141
import com.nextcloud.talk.application.NextcloudTalkApplication
142142
import com.nextcloud.talk.chat.data.model.ChatMessage
143+
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
143144
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
144145
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
145146
import com.nextcloud.talk.contextchat.ContextChatView
@@ -664,12 +665,76 @@ class ChatActivity :
664665
updateRemoteLastReadMessageIfNeeded = { updateRemoteLastReadMessageIfNeeded() },
665666
onLongClick = { openMessageActionsDialog(it) },
666667
onFileClick = { downloadAndOpenFile(it) },
667-
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) }
668+
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) },
669+
onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) },
670+
onVoiceSeek = { _, progress -> chatViewModel.seekToMediaPlayer(progress) },
671+
onVoiceSpeedClick = { onVoiceSpeedClickCompose(it) }
668672
)
669673
}
670674
}
671675
}
672676

677+
private fun onVoicePlayPauseClickCompose(messageId: Int) {
678+
lifecycleScope.launch {
679+
val isCurrentlyPlaying = chatViewModel.uiState.value.items
680+
.mapNotNull { (it as? ChatViewModel.ChatItem.MessageItem)?.uiMessage }
681+
.firstOrNull { it.id == messageId }
682+
?.content
683+
?.let { it as? MessageTypeContent.Voice }
684+
?.isPlaying ?: false
685+
686+
val message = chatViewModel.getMessageById(messageId.toLong()).first()
687+
val filename = message.fileParameters.name
688+
if (filename.isEmpty()) {
689+
return@launch
690+
}
691+
692+
val file = File(context.cacheDir, filename)
693+
if (file.exists()) {
694+
if (isCurrentlyPlaying) {
695+
chatViewModel.pauseMediaPlayer(true)
696+
chatViewModel.pauseVoiceMessageUiState(messageId)
697+
} else {
698+
val uiSpeed = chatViewModel.uiState.value.items
699+
.mapNotNull { (it as? ChatViewModel.ChatItem.MessageItem)?.uiMessage }
700+
.firstOrNull { it.id == messageId }
701+
?.content
702+
?.let { it as? MessageTypeContent.Voice }
703+
?.playbackSpeed ?: PlaybackSpeed.NORMAL
704+
chatViewModel.setPlayBack(uiSpeed)
705+
706+
val retrieved = appPreferences.getWaveFormFromFile(filename)
707+
if (retrieved.isEmpty()) {
708+
setUpWaveform(message)
709+
} else {
710+
if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) {
711+
message.voiceMessageFloatArray = retrieved.toFloatArray()
712+
chatViewModel.syncVoiceMessageUiState(message)
713+
}
714+
startPlayback(file, message)
715+
}
716+
}
717+
} else {
718+
downloadFileToCache(message, true) {
719+
setUpWaveform(message)
720+
}
721+
}
722+
}
723+
}
724+
725+
private fun onVoiceSpeedClickCompose(messageId: Int) {
726+
val currentSpeed = chatViewModel.uiState.value.items
727+
.mapNotNull { (it as? ChatViewModel.ChatItem.MessageItem)?.uiMessage }
728+
.firstOrNull { it.id == messageId }
729+
?.content
730+
?.let { it as? MessageTypeContent.Voice }
731+
?.playbackSpeed ?: PlaybackSpeed.NORMAL
732+
val nextSpeed = currentSpeed.next()
733+
chatViewModel.setPlayBack(nextSpeed)
734+
appPreferences.savePreferredPlayback(conversationUser!!.userId, nextSpeed)
735+
chatViewModel.setVoiceMessageSpeed(messageId, nextSpeed)
736+
}
737+
673738
fun downloadAndOpenFile(messageId: Int) {
674739
lifecycleScope.launch {
675740
val chatMessage = chatViewModel.getMessageById(messageId.toLong()).first()
@@ -1734,12 +1799,15 @@ class ChatActivity :
17341799
val file = File(context.cacheDir, filename!!)
17351800
if (file.exists() && message.voiceMessageFloatArray == null) {
17361801
message.isDownloadingVoiceMessage = true
1802+
chatViewModel.syncVoiceMessageUiState(message)
17371803
adapter?.update(message)
17381804
CoroutineScope(Dispatchers.Default).launch {
17391805
val r = AudioUtils.audioFileToFloatArray(file)
17401806
appPreferences.saveWaveFormForFile(filename, r.toTypedArray())
17411807
message.voiceMessageFloatArray = r
17421808
withContext(Dispatchers.Main) {
1809+
message.isDownloadingVoiceMessage = false
1810+
chatViewModel.syncVoiceMessageUiState(message)
17431811
startPlayback(file, message)
17441812
}
17451813
}
@@ -1753,6 +1821,7 @@ class ChatActivity :
17531821
chatViewModel.queueInMediaPlayer(file.canonicalPath, message)
17541822
chatViewModel.startCyclingMediaPlayer()
17551823
message.isPlayingVoiceMessage = true
1824+
chatViewModel.syncVoiceMessageUiState(message)
17561825
adapter?.update(message)
17571826

17581827
var pos = adapter?.getMessagePositionById(message.id)?.minus(1) ?: -1
@@ -2211,13 +2280,14 @@ class ChatActivity :
22112280
funToCallWhenDownloadSuccessful: (() -> Unit)
22122281
) {
22132282
message.isDownloadingVoiceMessage = true
2283+
chatViewModel.syncVoiceMessageUiState(message)
22142284
message.openWhenDownloaded = openWhenDownloaded
22152285
adapter?.update(message)
22162286

2217-
val baseUrl = message.activeUser!!.baseUrl
2218-
val userId = message.activeUser!!.userId
2287+
val baseUrl = conversationUser.baseUrl
2288+
val userId = conversationUser.userId
22192289
val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(
2220-
message.activeUser!!.capabilities!!
2290+
conversationUser.capabilities!!
22212291
.spreedCapability!!
22222292
)
22232293
val fileName = message.fileParameters.name

app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
2424
import kotlinx.coroutines.flow.StateFlow
2525
import kotlinx.coroutines.isActive
2626
import kotlinx.coroutines.launch
27-
import kotlinx.coroutines.runBlocking
2827
import kotlinx.coroutines.withContext
2928
import java.io.File
3029
import java.io.FileNotFoundException
@@ -89,6 +88,7 @@ class MediaPlayerManager : LifecycleAwareManager {
8988
private var currentDataSource: String = ""
9089
var mediaPlayerDuration: Int = 0
9190
var mediaPlayerPosition: Int = 0
91+
private var requestedPlaybackSpeed: PlaybackSpeed? = null
9292

9393
/**
9494
* Starts playing audio from the given path, initializes or resumes if the player is already created.
@@ -237,6 +237,7 @@ class MediaPlayerManager : LifecycleAwareManager {
237237
* Sets the player speed.
238238
*/
239239
fun setPlayBackSpeed(speed: PlaybackSpeed) {
240+
requestedPlaybackSpeed = speed
240241
if (mediaPlayer != null && mediaPlayer!!.isPlaying) {
241242
mediaPlayer!!.playbackParams.let { params ->
242243
params.setSpeed(speed.value)
@@ -292,11 +293,16 @@ class MediaPlayerManager : LifecycleAwareManager {
292293
currentCycledMessage?.let {
293294
it.resetVoiceMessage = true
294295
it.isPlayingVoiceMessage = false
296+
it.voiceMessageSeekbarProgress = 0
297+
it.voiceMessagePlayedSeconds = 0
295298
}
296-
runBlocking {
297-
_mediaPlayerSeekBarPositionMsg.emit(currentCycledMessage!!)
298-
}
299+
val completedMessage = currentCycledMessage
299300
currentCycledMessage = null
301+
if (completedMessage != null) {
302+
scope.launch {
303+
_mediaPlayerSeekBarPositionMsg.emit(completedMessage)
304+
}
305+
}
300306
loop = false
301307
_managerState.value = MediaPlayerManagerState.STOPPED
302308
}
@@ -311,12 +317,13 @@ class MediaPlayerManager : LifecycleAwareManager {
311317
private fun MediaPlayer.onPrepare() {
312318
mediaPlayerDuration = this.duration
313319

314-
val playBackSpeed = if (currentCycledMessage?.actorId == null) {
315-
PlaybackSpeed.NORMAL.value
316-
} else {
317-
appPreferences.getPreferredPlayback(currentCycledMessage?.actorId).value
318-
}
319-
mediaPlayer!!.playbackParams.setSpeed(playBackSpeed)
320+
val playBackSpeed = requestedPlaybackSpeed?.value
321+
?: if (currentCycledMessage?.actorId == null) {
322+
PlaybackSpeed.NORMAL.value
323+
} else {
324+
appPreferences.getPreferredPlayback(currentCycledMessage?.actorId).value
325+
}
326+
mediaPlayer!!.playbackParams = mediaPlayer!!.playbackParams.setSpeed(playBackSpeed)
320327

321328
start()
322329
_managerState.value = MediaPlayerManagerState.STARTED

app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ data class ChatMessage(
3838

3939
var isFormerOneToOneConversation: Boolean = false,
4040

41+
@Deprecated("should be deleted in long term")
4142
var activeUser: User? = null,
4243

4344
@Deprecated("delete with chatkit?")

app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.nextcloud.talk.data.database.model.SendStatus
1616
import com.nextcloud.talk.data.user.model.User
1717
import com.nextcloud.talk.utils.ApiUtils
1818
import com.nextcloud.talk.utils.DrawableUtils
19+
import com.nextcloud.talk.ui.PlaybackSpeed
1920
import java.time.LocalDate
2021

2122
// immutable class for chat message UI. only val, no vars!
@@ -59,8 +60,15 @@ sealed interface MessageTypeContent {
5960
MessageTypeContent
6061

6162
data class Voice(
62-
// TODO
63-
val todo: String
63+
val actorId: String?,
64+
val isPlaying: Boolean,
65+
val wasPlayed: Boolean,
66+
val isDownloading: Boolean,
67+
val durationSeconds: Int,
68+
val playedSeconds: Int,
69+
val seekbarProgress: Int,
70+
val waveform: List<Float>,
71+
val playbackSpeed: PlaybackSpeed = PlaybackSpeed.NORMAL
6472
) : MessageTypeContent
6573
}
6674

@@ -227,5 +235,12 @@ fun getDeckContent(message: ChatMessage): MessageTypeContent.Deck {
227235

228236
fun getVoiceContent(message: ChatMessage): MessageTypeContent.Voice =
229237
MessageTypeContent.Voice(
230-
todo = "still todo..."
238+
actorId = message.actorId,
239+
isPlaying = message.isPlayingVoiceMessage,
240+
wasPlayed = message.wasPlayedVoiceMessage,
241+
isDownloading = message.isDownloadingVoiceMessage,
242+
durationSeconds = message.voiceMessageDuration,
243+
playedSeconds = message.voiceMessagePlayedSeconds,
244+
seekbarProgress = message.voiceMessageSeekbarProgress,
245+
waveform = message.voiceMessageFloatArray?.toList().orEmpty()
231246
)

app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.nextcloud.talk.chat.data.io.MediaRecorderManager
2626
import com.nextcloud.talk.chat.data.model.ChatMessage
2727
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
2828
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
29+
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
2930
import com.nextcloud.talk.chat.ui.model.toUiModel
3031
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
3132
import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository
@@ -421,6 +422,81 @@ class ChatViewModel @AssistedInject constructor(
421422
init {
422423
observeConversation()
423424
observeMessages()
425+
observeMediaPlayerProgressForCompose()
426+
}
427+
428+
private fun observeMediaPlayerProgressForCompose() {
429+
mediaPlayerSeekbarObserver
430+
.onEach { message ->
431+
syncVoiceMessageUiState(message)
432+
}
433+
.launchIn(viewModelScope)
434+
}
435+
436+
fun pauseVoiceMessageUiState(messageId: Int) {
437+
_uiState.update { current ->
438+
val updatedItems = current.items.map { item ->
439+
if (item is ChatItem.MessageItem && item.uiMessage.id == messageId) {
440+
val voiceContent = item.uiMessage.content as? MessageTypeContent.Voice
441+
if (voiceContent != null) {
442+
item.copy(uiMessage = item.uiMessage.copy(content = voiceContent.copy(isPlaying = false)))
443+
} else {
444+
item
445+
}
446+
} else {
447+
item
448+
}
449+
}
450+
current.copy(items = updatedItems)
451+
}
452+
}
453+
454+
fun setVoiceMessageSpeed(messageId: Int, speed: PlaybackSpeed) {
455+
_uiState.update { current ->
456+
val updatedItems = current.items.map { item ->
457+
if (item is ChatItem.MessageItem && item.uiMessage.id == messageId) {
458+
val voiceContent = item.uiMessage.content as? MessageTypeContent.Voice
459+
if (voiceContent != null) {
460+
item.copy(uiMessage = item.uiMessage.copy(content = voiceContent.copy(playbackSpeed = speed)))
461+
} else {
462+
item
463+
}
464+
} else {
465+
item
466+
}
467+
}
468+
current.copy(items = updatedItems)
469+
}
470+
}
471+
472+
fun syncVoiceMessageUiState(message: ChatMessage) {
473+
_uiState.update { current ->
474+
val updatedItems = current.items.map { item ->
475+
if (item is ChatItem.MessageItem && item.uiMessage.id == message.jsonMessageId) {
476+
val voiceContent = item.uiMessage.content as? MessageTypeContent.Voice
477+
if (voiceContent != null) {
478+
val updatedVoiceContent = voiceContent.copy(
479+
actorId = message.actorId,
480+
isPlaying = message.isPlayingVoiceMessage,
481+
wasPlayed = message.wasPlayedVoiceMessage,
482+
isDownloading = message.isDownloadingVoiceMessage,
483+
durationSeconds = message.voiceMessageDuration,
484+
playedSeconds = message.voiceMessagePlayedSeconds,
485+
seekbarProgress = message.voiceMessageSeekbarProgress,
486+
waveform = message.voiceMessageFloatArray?.toList() ?: voiceContent.waveform
487+
// playbackSpeed is preserved from existing voiceContent
488+
)
489+
item.copy(uiMessage = item.uiMessage.copy(content = updatedVoiceContent))
490+
} else {
491+
item
492+
}
493+
} else {
494+
item
495+
}
496+
}
497+
498+
current.copy(items = updatedItems)
499+
}
424500
}
425501

426502
// ------------------------------

app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ fun ChatMessageView(
3030
conversationThreadId: Long? = null,
3131
onLongClick: ((Int) -> Unit?)? = null,
3232
onFileClick: (Int) -> Unit = {},
33-
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> }
33+
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> },
34+
onVoicePlayPauseClick: (Int) -> Unit = {},
35+
onVoiceSeek: (messageId: Int, progress: Int) -> Unit = { _, _ -> },
36+
onVoiceSpeedClick: (Int) -> Unit = {}
3437
) {
3538
Box(
3639
modifier = Modifier
@@ -85,7 +88,10 @@ fun ChatMessageView(
8588
typeContent = content,
8689
message = message,
8790
isOneToOneConversation = isOneToOneConversation,
88-
conversationThreadId = conversationThreadId
91+
conversationThreadId = conversationThreadId,
92+
onPlayPauseClick = onVoicePlayPauseClick,
93+
onSeek = onVoiceSeek,
94+
onSpeedClick = onVoiceSpeedClick
8995
)
9096
}
9197

@@ -191,7 +197,18 @@ private fun ChatMessageViewGeolocationPreview() {
191197
@Composable
192198
private fun ChatMessageViewVoicePreview() {
193199
PreviewContainer {
194-
val uiMessage = createBaseMessage(MessageTypeContent.Voice(todo = "preview"))
200+
val uiMessage = createBaseMessage(
201+
MessageTypeContent.Voice(
202+
actorId = "john",
203+
isPlaying = false,
204+
wasPlayed = false,
205+
isDownloading = false,
206+
durationSeconds = 16,
207+
playedSeconds = 4,
208+
seekbarProgress = 25,
209+
waveform = listOf(0.1f, 0.2f, 0.4f, 0.15f, 0.3f, 0.5f)
210+
)
211+
)
195212
ChatMessageView(message = uiMessage)
196213
}
197214
}

app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ fun GetNewChatView(
7171
updateRemoteLastReadMessageIfNeeded: (() -> Unit?)?,
7272
onLongClick: ((Int) -> Unit?)?,
7373
onFileClick: (Int) -> Unit,
74-
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> }
74+
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> },
75+
onVoicePlayPauseClick: (Int) -> Unit = {},
76+
onVoiceSeek: (messageId: Int, progress: Int) -> Unit = { _, _ -> },
77+
onVoiceSpeedClick: (Int) -> Unit = {}
7578
) {
7679
val viewThemeUtils = LocalViewThemeUtils.current
7780
val colorScheme = viewThemeUtils.getColorScheme(LocalContext.current)
@@ -249,7 +252,10 @@ fun GetNewChatView(
249252
conversationThreadId = conversationThreadId,
250253
onFileClick = onFileClick,
251254
onLongClick = onLongClick,
252-
onPollClick = onPollClick
255+
onPollClick = onPollClick,
256+
onVoicePlayPauseClick = onVoicePlayPauseClick,
257+
onVoiceSeek = onVoiceSeek,
258+
onVoiceSpeedClick = onVoiceSpeedClick
253259
)
254260
}
255261

0 commit comments

Comments
 (0)