mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-12-19 23:22:44 +03:00
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:
parent
187f98ad6b
commit
20d36c1eb9
13 changed files with 260 additions and 34 deletions
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue