playback speed control button for voice messages

Signed-off-by: Christian Reiner <foss@christian-reiner.info>

Themed the PlaybackSpeedControl + Work around onBind bug

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
This commit is contained in:
Christian Reiner 2024-11-21 21:52:37 +01:00 committed by Andy Scherzinger
parent 187f98ad6b
commit 20d36c1eb9
No known key found for this signature in database
GPG key ID: 6CADC7E3523C308B
13 changed files with 260 additions and 34 deletions

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me> * SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
@ -68,10 +69,16 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
lateinit var voiceMessageInterface: VoiceMessageInterface lateinit var voiceMessageInterface: VoiceMessageInterface
lateinit var commonMessageInterface: CommonMessageInterface lateinit var commonMessageInterface: CommonMessageInterface
private var isBound = false
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) { override fun onBind(message: ChatMessage) {
super.onBind(message) super.onBind(message)
if (isBound) {
handleIsPlayingVoiceMessageState(message)
return
}
this.message = message this.message = message
sharedApplication!!.componentApplication.inject(this) sharedApplication!!.componentApplication.inject(this)
@ -100,25 +107,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
if (message.isPlayingVoiceMessage) {
showPlayButton()
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!,
R.drawable.ic_baseline_pause_voice_message_24
)
val d = message.voiceMessageDuration.toLong()
val t = message.voiceMessagePlayedSeconds.toLong()
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
binding.voiceMessageDuration.visibility = View.VISIBLE
binding.seekbar.progress = message.voiceMessageSeekbarProgress
} else {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!,
R.drawable.ic_baseline_play_arrow_voice_message_24
)
}
if (message.isDownloadingVoiceMessage) { if (message.isDownloadingVoiceMessage) {
showVoiceMessageLoading() showVoiceMessageLoading()
} else { } else {
@ -158,6 +146,10 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
} }
}) })
voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed ->
binding.playbackSpeedControlBtn.setSpeed(speed)
}
Reaction().showReactions( Reaction().showReactions(
message, message,
::clickOnReaction, ::clickOnReaction,
@ -167,6 +159,8 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
false, false,
viewThemeUtils viewThemeUtils
) )
isBound = true
} }
private fun longClickOnReaction(chatMessage: ChatMessage) { private fun longClickOnReaction(chatMessage: ChatMessage) {
@ -177,6 +171,29 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji) commonMessageInterface.onClickReaction(chatMessage, emoji)
} }
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
if (message.isPlayingVoiceMessage) {
showPlayButton()
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!,
R.drawable.ic_baseline_pause_voice_message_24
)
val d = message.voiceMessageDuration.toLong()
val t = message.voiceMessagePlayedSeconds.toLong()
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
binding.voiceMessageDuration.visibility = View.VISIBLE
binding.seekbar.max = message.voiceMessageDuration * ONE_SEC
binding.seekbar.progress = message.voiceMessageSeekbarProgress
} else {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!,
R.drawable.ic_baseline_play_arrow_voice_message_24
)
}
}
private fun updateDownloadState(message: ChatMessage) { private fun updateDownloadState(message: ChatMessage) {
// check if download worker is already running // check if download worker is already running
val fileId = message.selectedIndividualHashMap!!["id"] val fileId = message.selectedIndividualHashMap!!["id"]

View file

@ -74,10 +74,16 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
lateinit var voiceMessageInterface: VoiceMessageInterface lateinit var voiceMessageInterface: VoiceMessageInterface
lateinit var commonMessageInterface: CommonMessageInterface lateinit var commonMessageInterface: CommonMessageInterface
private var isBound = false
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) { override fun onBind(message: ChatMessage) {
super.onBind(message) super.onBind(message)
if (isBound) {
handleIsPlayingVoiceMessageState(message)
return
}
this.message = message this.message = message
sharedApplication!!.componentApplication.inject(this) sharedApplication!!.componentApplication.inject(this)
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
@ -102,12 +108,9 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
setParentMessageDataOnMessageItem(message) setParentMessageDataOnMessageItem(message)
updateDownloadState(message) updateDownloadState(message)
binding.seekbar.max = message.voiceMessageDuration * ONE_SEC
viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar)
viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT)
handleIsPlayingVoiceMessageState(message)
handleIsDownloadingVoiceMessageState(message) handleIsDownloadingVoiceMessageState(message)
handleResetVoiceMessageState(message) handleResetVoiceMessageState(message)
@ -149,6 +152,10 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
binding.checkMark.contentDescription = readStatusContentDescriptionString binding.checkMark.contentDescription = readStatusContentDescriptionString
voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed ->
binding.playbackSpeedControlBtn.setSpeed(speed)
}
Reaction().showReactions( Reaction().showReactions(
message, message,
::clickOnReaction, ::clickOnReaction,
@ -158,6 +165,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
true, true,
viewThemeUtils viewThemeUtils
) )
isBound = true
} }
private fun longClickOnReaction(chatMessage: ChatMessage) { private fun longClickOnReaction(chatMessage: ChatMessage) {
@ -207,6 +215,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
val t = message.voiceMessagePlayedSeconds.toLong() val t = message.voiceMessagePlayedSeconds.toLong()
binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t)
binding.voiceMessageDuration.visibility = View.VISIBLE binding.voiceMessageDuration.visibility = View.VISIBLE
binding.seekbar.max = message.voiceMessageDuration * ONE_SEC
binding.seekbar.progress = message.voiceMessageSeekbarProgress binding.seekbar.progress = message.voiceMessageSeekbarProgress
} else { } else {
binding.playPauseBtn.visibility = View.VISIBLE binding.playPauseBtn.visibility = View.VISIBLE

View file

@ -1,13 +1,16 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.ui.PlaybackSpeed
interface VoiceMessageInterface { interface VoiceMessageInterface {
fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int)
fun registerMessageToObservePlaybackSpeedPreferences(userId: String, listener: (speed: PlaybackSpeed) -> Unit)
} }

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com> * SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
* SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com> * SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com>
* SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com> * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
@ -136,6 +137,8 @@ import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.signaling.SignalingMessageReceiver import com.nextcloud.talk.signaling.SignalingMessageReceiver
import com.nextcloud.talk.signaling.SignalingMessageSender import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.translate.ui.TranslateActivity import com.nextcloud.talk.translate.ui.TranslateActivity
import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.DateTimePickerFragment import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
@ -205,6 +208,7 @@ import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
import kotlin.String
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -357,6 +361,19 @@ class ChatActivity :
private var voiceMessageToRestoreAudioPosition = 0 private var voiceMessageToRestoreAudioPosition = 0
private var voiceMessageToRestoreWasPlaying = false private var voiceMessageToRestoreWasPlaying = false
private val playbackSpeedPreferencesObserver: (Map<String, PlaybackSpeed>) -> Unit = { speedPreferenceLiveData ->
mediaPlayer?.let { mediaPlayer ->
(mediaPlayer.isPlaying == true).also {
currentlyPlayedVoiceMessage?.let { message ->
mediaPlayer.playbackParams.let { params ->
params.setSpeed(chatViewModel.getPlaybackSpeedPreference(message).value)
mediaPlayer.playbackParams = params
}
}
}
}
}
private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener { private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
override fun onSwitchTo(token: String?) { override fun onSwitchTo(token: String?) {
if (token != null) { if (token != null) {
@ -434,6 +451,10 @@ class ChatActivity :
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
appPreferences.readVoiceMessagePlaybackSpeedPreferences().let { playbackSpeedPreferences ->
chatViewModel.applyPlaybackSpeedPreferences(playbackSpeedPreferences)
}
initObservers() initObservers()
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -1045,6 +1066,8 @@ class ChatActivity :
setupSwipeToReply() setupSwipeToReply()
chatViewModel.voiceMessagePlaybackSpeedPreferences.observe(this, playbackSpeedPreferencesObserver)
binding.unreadMessagesPopup.setOnClickListener { binding.unreadMessagesPopup.setOnClickListener {
binding.messagesListView.smoothScrollToPosition(0) binding.messagesListView.smoothScrollToPosition(0)
binding.unreadMessagesPopup.visibility = View.GONE binding.unreadMessagesPopup.visibility = View.GONE
@ -1131,6 +1154,7 @@ class ChatActivity :
adapter?.setLoadMoreListener(this) adapter?.setLoadMoreListener(this)
adapter?.setDateHeadersFormatter { format(it) } adapter?.setDateHeadersFormatter { format(it) }
adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
adapter?.registerViewClickListener( adapter?.registerViewClickListener(
R.id.playPauseBtn R.id.playPauseBtn
) { _, message -> ) { _, message ->
@ -1154,6 +1178,15 @@ class ChatActivity :
} }
} }
} }
adapter?.registerViewClickListener(R.id.playbackSpeedControlBtn) { button, message ->
val nextSpeed = (button as PlaybackSpeedControl).getSpeed().next()
HashMap(appPreferences.readVoiceMessagePlaybackSpeedPreferences()).let { playbackSpeedPreferences ->
playbackSpeedPreferences[message.user.id] = nextSpeed
chatViewModel.applyPlaybackSpeedPreferences(playbackSpeedPreferences)
appPreferences.saveVoiceMessagePlaybackSpeedPreferences(playbackSpeedPreferences)
}
}
} }
private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true) { private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true) {
@ -1579,6 +1612,9 @@ class ChatActivity :
mediaPlayer?.let { mediaPlayer?.let {
if (!it.isPlaying && doPlay) { if (!it.isPlaying && doPlay) {
chatViewModel.audioRequest(true) { chatViewModel.audioRequest(true) {
it.playbackParams = it.playbackParams.apply {
setSpeed(chatViewModel.getPlaybackSpeedPreference(message).value)
}
it.start() it.start()
} }
} }
@ -1703,6 +1739,20 @@ class ChatActivity :
} }
} }
override fun registerMessageToObservePlaybackSpeedPreferences(
userId: String,
listener: (speed: PlaybackSpeed) -> Unit
) {
chatViewModel.voiceMessagePlaybackSpeedPreferences.let { liveData ->
liveData.observe(this) { playbackSpeedPreferences ->
listener(playbackSpeedPreferences[userId] ?: PlaybackSpeed.NORMAL)
}
liveData.value?.let { playbackSpeedPreferences ->
listener(playbackSpeedPreferences[userId] ?: PlaybackSpeed.NORMAL)
}
}
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun collapseSystemMessages() { override fun collapseSystemMessages() {
adapter?.items?.forEach { adapter?.items?.forEach {
@ -2372,6 +2422,8 @@ class ChatActivity :
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup() mentionAutocomplete?.dismissPopup()
} }
chatViewModel.voiceMessagePlaybackSpeedPreferences.removeObserver(playbackSpeedPreferencesObserver)
} }
private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -33,6 +34,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
@ -107,6 +109,10 @@ class ChatViewModel @Inject constructor(
val getVoiceRecordingLocked: LiveData<Boolean> val getVoiceRecordingLocked: LiveData<Boolean>
get() = _getVoiceRecordingLocked get() = _getVoiceRecordingLocked
private val _voiceMessagePlaybackSpeeds: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData()
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
get() = _voiceMessagePlaybackSpeeds
val getMessageFlow = chatRepository.messageFlow val getMessageFlow = chatRepository.messageFlow
.onEach { .onEach {
_chatMessageViewState.value = if (_chatMessageViewState.value == ChatMessageInitialState) { _chatMessageViewState.value = if (_chatMessageViewState.value == ChatMessageInitialState) {
@ -644,6 +650,13 @@ class ChatViewModel @Inject constructor(
emit(message.first()) emit(message.first())
} }
fun applyPlaybackSpeedPreferences(speeds: Map<String, PlaybackSpeed>) {
_voiceMessagePlaybackSpeeds.postValue(speeds)
}
fun getPlaybackSpeedPreference(message: ChatMessage) =
_voiceMessagePlaybackSpeeds.value?.get(message.user.id) ?: PlaybackSpeed.NORMAL
// inner class GetRoomObserver : Observer<ConversationModel> { // inner class GetRoomObserver : Observer<ConversationModel> {
// override fun onSubscribe(d: Disposable) { // override fun onSubscribe(d: Disposable) {
// // unused atm // // unused atm

View file

@ -38,7 +38,7 @@ class MessageInputViewModel @Inject constructor(
private val audioRecorderManager: AudioRecorderManager, private val audioRecorderManager: AudioRecorderManager,
private val mediaPlayerManager: MediaPlayerManager, private val mediaPlayerManager: MediaPlayerManager,
private val audioFocusRequestManager: AudioFocusRequestManager, private val audioFocusRequestManager: AudioFocusRequestManager,
private val dataStore: AppPreferences private val appPreferences: AppPreferences
) : ViewModel(), DefaultLifecycleObserver { ) : ViewModel(), DefaultLifecycleObserver {
enum class LifeCycleFlag { enum class LifeCycleFlag {
PAUSED, PAUSED,
@ -147,9 +147,9 @@ class MessageInputViewModel @Inject constructor(
if (isQueueing) { if (isQueueing) {
val tempID = System.currentTimeMillis().toInt() val tempID = System.currentTimeMillis().toInt()
val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification) val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification)
messageQueue = dataStore.getMessageQueue(internalId) messageQueue = appPreferences.getMessageQueue(internalId)
messageQueue.add(qMsg) messageQueue.add(qMsg)
dataStore.saveMessageQueue(internalId, messageQueue) appPreferences.saveMessageQueue(internalId, messageQueue)
_messageQueueSizeFlow.update { messageQueue.size } _messageQueueSizeFlow.update { messageQueue.size }
_messageQueueFlow.postValue(listOf(qMsg)) _messageQueueFlow.postValue(listOf(qMsg))
return return
@ -260,8 +260,8 @@ class MessageInputViewModel @Inject constructor(
if (isQueueing) return if (isQueueing) return
messageQueue.clear() messageQueue.clear()
val queue = dataStore.getMessageQueue(internalId) val queue = appPreferences.getMessageQueue(internalId)
dataStore.saveMessageQueue(internalId, null) // empties the queue appPreferences.saveMessageQueue(internalId, null) // empties the queue
while (queue.size > 0) { while (queue.size > 0) {
val msg = queue.removeAt(0) val msg = queue.removeAt(0)
sendChatMessage( sendChatMessage(
@ -279,7 +279,7 @@ class MessageInputViewModel @Inject constructor(
} }
fun getTempMessagesFromMessageQueue(internalId: String) { fun getTempMessagesFromMessageQueue(internalId: String) {
val queue = dataStore.getMessageQueue(internalId) val queue = appPreferences.getMessageQueue(internalId)
val list = mutableListOf<QueuedMessage>() val list = mutableListOf<QueuedMessage>()
for (msg in queue) { for (msg in queue) {
list.add(msg) list.add(msg)
@ -292,31 +292,31 @@ class MessageInputViewModel @Inject constructor(
} }
fun restoreMessageQueue(internalId: String) { fun restoreMessageQueue(internalId: String) {
messageQueue = dataStore.getMessageQueue(internalId) messageQueue = appPreferences.getMessageQueue(internalId)
_messageQueueSizeFlow.tryEmit(messageQueue.size) _messageQueueSizeFlow.tryEmit(messageQueue.size)
} }
fun removeFromQueue(internalId: String, id: Int) { fun removeFromQueue(internalId: String, id: Int) {
val queue = dataStore.getMessageQueue(internalId) val queue = appPreferences.getMessageQueue(internalId)
for (qMsg in queue) { for (qMsg in queue) {
if (qMsg.id == id) { if (qMsg.id == id) {
queue.remove(qMsg) queue.remove(qMsg)
break break
} }
} }
dataStore.saveMessageQueue(internalId, queue) appPreferences.saveMessageQueue(internalId, queue)
_messageQueueSizeFlow.tryEmit(queue.size) _messageQueueSizeFlow.tryEmit(queue.size)
} }
fun editQueuedMessage(internalId: String, id: Int, newMessage: String) { fun editQueuedMessage(internalId: String, id: Int, newMessage: String) {
val queue = dataStore.getMessageQueue(internalId) val queue = appPreferences.getMessageQueue(internalId)
for (qMsg in queue) { for (qMsg in queue) {
if (qMsg.id == id) { if (qMsg.id == id) {
qMsg.message = newMessage qMsg.message = newMessage
break break
} }
} }
dataStore.saveMessageQueue(internalId, queue) appPreferences.saveMessageQueue(internalId, queue)
} }
fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) { fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) {

View file

@ -0,0 +1,75 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.ui
import android.content.Context
import android.util.AttributeSet
import autodagger.AutoInjector
import com.google.android.material.button.MaterialButton
import java.util.Locale
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import javax.inject.Inject
internal const val SPEED_FACTOR_SLOW = 0.8f
internal const val SPEED_FACTOR_NORMAL = 1.0f
internal const val SPEED_FACTOR_FASTER = 1.5f
internal const val SPEED_FACTOR_FASTEST = 2.0f
@AutoInjector(NextcloudTalkApplication::class)
class PlaybackSpeedControl @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MaterialButton(context, attrs, defStyleAttr) {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
private var currentSpeed = PlaybackSpeed.NORMAL
init {
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
text = currentSpeed.label
viewThemeUtils.material.colorMaterialButtonText(this)
}
fun setSpeed(newSpeed: PlaybackSpeed) {
currentSpeed = newSpeed
text = currentSpeed.label
}
fun getSpeed(): PlaybackSpeed {
return currentSpeed
}
}
enum class PlaybackSpeed(val value: Float) {
SLOW(SPEED_FACTOR_SLOW),
NORMAL(SPEED_FACTOR_NORMAL),
FASTER(SPEED_FACTOR_FASTER),
FASTEST(SPEED_FACTOR_FASTEST);
// no fixed, literal labels, since we want to obey numeric interpunctuation for different locales
val label: String = String.format(Locale.getDefault(), "%01.1fx", value)
fun next(): PlaybackSpeed {
return entries[(ordinal + 1) % entries.size]
}
companion object {
fun byName(name: String): PlaybackSpeed {
for (speed in entries) {
if (speed.name.equals(name, ignoreCase = true)) {
return speed
}
}
return NORMAL
}
}
}

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me> * SPDX-FileCopyrightText: 2021 Tim Krüger <t@timkrueger.me>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com> * SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
@ -11,8 +12,10 @@ package com.nextcloud.talk.utils.preferences;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel; import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel;
import com.nextcloud.talk.ui.PlaybackSpeed;
import java.util.List; import java.util.List;
import java.util.Map;
@SuppressLint("NonConstantResourceId") @SuppressLint("NonConstantResourceId")
public interface AppPreferences { public interface AppPreferences {
@ -178,6 +181,10 @@ public interface AppPreferences {
void deleteAllMessageQueuesFor(String userId); void deleteAllMessageQueuesFor(String userId);
void saveVoiceMessagePlaybackSpeedPreferences(Map<String, PlaybackSpeed> speeds);
Map<String, PlaybackSpeed> readVoiceMessagePlaybackSpeedPreferences();
Long getNotificationWarningLastPostponedDate(); Long getNotificationWarningLastPostponedDate();
void setNotificationWarningLastPostponedDate(Long showNotificationWarning); void setNotificationWarningLastPostponedDate(Long showNotificationWarning);

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
* SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@nextcloud.com> * SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@nextcloud.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -17,12 +18,16 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.ui.PlaybackSpeed
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "DeferredResultUnused", "EmptyFunctionBlock") @Suppress("TooManyFunctions", "DeferredResultUnused", "EmptyFunctionBlock")
@ -565,6 +570,26 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
} }
} }
override fun saveVoiceMessagePlaybackSpeedPreferences(speeds: Map<String, PlaybackSpeed>) {
Json.encodeToString(speeds).let {
runBlocking<Unit> { async { writeString(VOICE_MESSAGE_PLAYBACK_SPEEDS, it) } }
}
}
override fun readVoiceMessagePlaybackSpeedPreferences(): Map<String, PlaybackSpeed> {
return runBlocking {
async { readString(VOICE_MESSAGE_PLAYBACK_SPEEDS, "{}").first() }
}.getCompleted().let {
try {
Json.decodeFromString<HashMap<String, String>>(it)
.map { entry -> entry.key to PlaybackSpeed.byName(entry.value) }.toMap()
} catch (e: SerializationException) {
Log.e(TAG, "ignoring invalid json format in voice message playback speed preferences", e)
emptyMap()
}
}
}
override fun getNotificationWarningLastPostponedDate(): Long = override fun getNotificationWarningLastPostponedDate(): Long =
runBlocking { runBlocking {
async { readLong(LAST_NOTIFICATION_WARNING).first() } async { readLong(LAST_NOTIFICATION_WARNING).first() }
@ -661,6 +686,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run" const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run"
const val TYPING_STATUS = "typing_status" const val TYPING_STATUS = "typing_status"
const val MESSAGE_QUEUE = "@message_queue" const val MESSAGE_QUEUE = "@message_queue"
const val VOICE_MESSAGE_PLAYBACK_SPEEDS = "voice_message_playback_speeds"
const val SHOW_REGULAR_NOTIFICATION_WARNING = "show_regular_notification_warning" const val SHOW_REGULAR_NOTIFICATION_WARNING = "show_regular_notification_warning"
const val LAST_NOTIFICATION_WARNING = "last_notification_warning" const val LAST_NOTIFICATION_WARNING = "last_notification_warning"
private fun String.convertStringToArray(): Array<Float> { private fun String.convertStringToArray(): Array<Float> {

View file

@ -50,6 +50,7 @@
tools:progress="50" tools:progress="50"
tools:progressTint="@color/hwSecurityRed" tools:progressTint="@color/hwSecurityRed"
tools:progressBackgroundTint="@color/blue"/> tools:progressBackgroundTint="@color/blue"/>
</LinearLayout> </LinearLayout>
<Chronometer <Chronometer

View file

@ -2,6 +2,7 @@
<!-- <!--
~ Nextcloud Talk - Android Client ~ Nextcloud Talk - Android Client
~ ~
~ SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de> ~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de> ~ SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com> ~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
@ -76,7 +77,6 @@
app:iconSize="40dp" app:iconSize="40dp"
app:iconTint="@color/nc_incoming_text_default" /> app:iconTint="@color/nc_incoming_text_default" />
<com.nextcloud.talk.ui.WaveformSeekBar <com.nextcloud.talk.ui.WaveformSeekBar
android:id="@+id/seekbar" android:id="@+id/seekbar"
android:layout_width="0dp" android:layout_width="0dp"
@ -85,6 +85,16 @@
tools:progress="50" tools:progress="50"
android:layout_weight="1" /> android:layout_weight="1" />
<com.nextcloud.talk.ui.PlaybackSpeedControl
android:id="@+id/playbackSpeedControlBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@string/playback_speed_control"
android:textColor="@color/black"
app:cornerRadius="@dimen/button_corner_radius"
app:rippleColor="#1FFFFFFF" />
</LinearLayout> </LinearLayout>

View file

@ -2,6 +2,7 @@
<!-- <!--
~ Nextcloud Talk - Android Client ~ Nextcloud Talk - Android Client
~ ~
~ SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de> ~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de> ~ SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com> ~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
@ -70,6 +71,17 @@
tools:progress="50" tools:progress="50"
android:layout_weight="1" /> android:layout_weight="1" />
<com.nextcloud.talk.ui.PlaybackSpeedControl
android:id="@+id/playbackSpeedControlBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@string/playback_speed_control"
android:textColor="@color/black"
app:cornerRadius="@dimen/button_corner_radius"
app:rippleColor="#1FFFFFFF" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View file

@ -572,6 +572,7 @@ How to translate with transifex:
<string name="nc_voice_message_slide_to_cancel">« Slide to cancel</string> <string name="nc_voice_message_slide_to_cancel">« Slide to cancel</string>
<string name="play_pause_voice_message">Play/pause voice message</string> <string name="play_pause_voice_message">Play/pause voice message</string>
<string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string> <string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string>
<string name="playback_speed_control">Playback speed control</string>
<!-- Phonebook Integration --> <!-- Phonebook Integration -->
<string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut into system contacts app</string> <string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut into system contacts app</string>