diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 7731e32f1f4..aa597eebad7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -81,6 +81,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -136,6 +137,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.ui.ShowReactionsModalBottomSheet import com.nextcloud.talk.chat.data.model.FileParameters import com.nextcloud.talk.chat.ui.MessageActionsBottomSheet import com.nextcloud.talk.chat.ui.ProfileModalBottomSheet @@ -188,6 +190,7 @@ import com.nextcloud.talk.ui.dialog.DateTimeCompose import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment +import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog import com.nextcloud.talk.ui.theme.LocalMessageUtils import com.nextcloud.talk.ui.theme.LocalOpenGraphFetcher import com.nextcloud.talk.ui.theme.LocalViewThemeUtils @@ -746,6 +749,8 @@ class ChatActivity : LocalMessageUtils provides messageUtils, LocalOpenGraphFetcher provides { url -> chatViewModel.fetchOpenGraph(url) } ) { + val currentlyPlayingId by chatViewModel.currentlyPlayedMessageId.collectAsState(null) + val isOneToOneConversation = uiState.isOneToOneConversation Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation) @@ -765,6 +770,7 @@ class ChatActivity : state = ChatViewState( chatItems = uiState.items, isOneToOneConversation = isOneToOneConversation, + currentlyPlayingVoiceMessageId = currentlyPlayingId, conversationThreadId = conversationThreadId, chatMode = chatMode, highlightedMessageId = uiState.highlightedMessageId, diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt index f8c0bfd28b5..c7084560977 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt @@ -84,7 +84,11 @@ class MediaPlayerManager : LifecycleAwareManager { private var mediaPlayer: MediaPlayer? = null private var loop = false private var scope = MainScope() - private var currentCycledMessage: ChatMessage? = null + + private val _currentCycledMessage = MutableStateFlow(null) + val currentCycledMessage: StateFlow + get() = _currentCycledMessage + private var currentDataSource: String = "" var mediaPlayerDuration: Int = 0 var mediaPlayerPosition: Int = 0 @@ -140,7 +144,7 @@ class MediaPlayerManager : LifecycleAwareManager { mediaPlayer!!.stop() mediaPlayer!!.release() mediaPlayer = null - currentCycledMessage = null + _currentCycledMessage.value = null _backgroundPlayUIFlow.tryEmit(null) _managerState.value = MediaPlayerManagerState.STOPPED } @@ -174,8 +178,8 @@ class MediaPlayerManager : LifecycleAwareManager { private suspend fun seekbarUpdateObserver() { withContext(Dispatchers.IO) { - currentCycledMessage?.voiceMessageDuration = mediaPlayerDuration / ONE_SEC - currentCycledMessage?.resetVoiceMessage = false + _currentCycledMessage.value?.voiceMessageDuration = mediaPlayerDuration / ONE_SEC + _currentCycledMessage.value?.resetVoiceMessage = false while (true) { if (!loop) { // NOTE: ok so this doesn't stop the loop, but rather stop the update. Wasteful, but minimal @@ -197,7 +201,7 @@ class MediaPlayerManager : LifecycleAwareManager { val progressI = ceil(progress).toInt() val seconds = (pos / ONE_SEC) _mediaPlayerSeekBarPosition.emit(progressI) - currentCycledMessage?.let { msg -> + _currentCycledMessage.value?.let { msg -> msg.isPlayingVoiceMessage = true msg.voiceMessageSeekbarProgress = progressI msg.voiceMessagePlayedSeconds = seconds @@ -240,7 +244,7 @@ class MediaPlayerManager : LifecycleAwareManager { requestedPlaybackSpeed = speed if (mediaPlayer != null && mediaPlayer!!.isPlaying) { mediaPlayer!!.playbackParams.let { params -> - params.setSpeed(speed.value) + params.speed = speed.value mediaPlayer!!.playbackParams = params } } @@ -270,7 +274,7 @@ class MediaPlayerManager : LifecycleAwareManager { val pair = playQueue.iterator().next() setDataSource(pair.first) currentDataSource = pair.first - currentCycledMessage = pair.second + _currentCycledMessage.value = pair.second playQueue.removeAt(0) prepareAsync() setOnPreparedListener { @@ -284,20 +288,20 @@ class MediaPlayerManager : LifecycleAwareManager { playQueue.removeAt(0) mediaPlayer?.reset() mediaPlayer?.setDataSource(nextPair.first) - currentCycledMessage = nextPair.second + _currentCycledMessage.value = nextPair.second prepare() } else { mediaPlayer?.release() mediaPlayer = null _backgroundPlayUIFlow.tryEmit(null) - currentCycledMessage?.let { + _currentCycledMessage.value?.let { it.resetVoiceMessage = true it.isPlayingVoiceMessage = false it.voiceMessageSeekbarProgress = 0 it.voiceMessagePlayedSeconds = 0 } - val completedMessage = currentCycledMessage - currentCycledMessage = null + val completedMessage = _currentCycledMessage.value + _currentCycledMessage.value = null if (completedMessage != null) { scope.launch { _mediaPlayerSeekBarPositionMsg.emit(completedMessage) @@ -318,16 +322,16 @@ class MediaPlayerManager : LifecycleAwareManager { mediaPlayerDuration = this.duration val playBackSpeed = requestedPlaybackSpeed?.value - ?: if (currentCycledMessage?.actorId == null) { + ?: if (_currentCycledMessage.value?.actorId == null) { PlaybackSpeed.NORMAL.value } else { - appPreferences.getPreferredPlayback(currentCycledMessage?.actorId).value + appPreferences.getPreferredPlayback(_currentCycledMessage.value?.actorId).value } mediaPlayer!!.playbackParams = mediaPlayer!!.playbackParams.setSpeed(playBackSpeed) start() _managerState.value = MediaPlayerManagerState.STARTED - currentCycledMessage?.let { + _currentCycledMessage.value?.let { it.isPlayingVoiceMessage = true _backgroundPlayUIFlow.tryEmit(it) } @@ -348,9 +352,9 @@ class MediaPlayerManager : LifecycleAwareManager { override fun handleOnStop() { loop = false - if (mediaPlayer != null && currentCycledMessage != null && mediaPlayer!!.isPlaying) { + if (mediaPlayer != null && _currentCycledMessage.value != null && mediaPlayer!!.isPlaying) { CoroutineScope(Dispatchers.Default).launch { - _backgroundPlayUIFlow.tryEmit(currentCycledMessage!!) + _backgroundPlayUIFlow.tryEmit(_currentCycledMessage.value) } } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index b9cb8978942..5a36ea4fb03 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -18,6 +18,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson +import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager @@ -28,7 +29,6 @@ import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.chat.ui.model.ChatMessageUi import com.nextcloud.talk.chat.ui.model.MessageTypeContent import com.nextcloud.talk.chat.ui.model.toUiModel -import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.conversationlist.DirectShareHelper import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository @@ -74,6 +74,8 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -88,12 +90,12 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -249,6 +251,10 @@ class ChatViewModel @AssistedInject constructor( val mediaPlayerSeekbarObserver: Flow get() = mediaPlayerManager.mediaPlayerSeekBarPositionMsg + // FIXME - map this to string id or some other kinda of id idk + val currentlyPlayedMessageId: Flow + get() = mediaPlayerManager.currentCycledMessage.map { msg -> msg?.jsonMessageId } + val managerStateFlow: Flow get() = mediaPlayerManager.managerState diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeWaveformSeekbar.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeWaveformSeekbar.kt index fe6a7e21b2a..bea05256ce7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeWaveformSeekbar.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeWaveformSeekbar.kt @@ -54,7 +54,7 @@ fun ComposeWaveformSeekBar(value: Float, onValueChange: (Float) -> Unit, modifie for (i in waveData.indices) { val x: Float = i * (barWidth + barGap) val y: Float = waveData[i] * height - val isXBeforeThumb = (x / this.size.width) <= value + OVERLAP + val isXBeforeThumb = (x / this.size.width) <= value drawLine( if (isXBeforeThumb) inversePrimary else onPrimaryContainer, @@ -85,7 +85,7 @@ fun Preview() { val waveData = remember { FloatArray(WAVEFORM_SIZE) { (Math.random() % 1).toFloat() } } ComposeWaveformSeekBar( - 0f, + 0.0f, {}, modifier = Modifier .height(MAX_HEIGHT.dp) diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt index 3672ca8a8b2..ca9cc8bd061 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt @@ -52,6 +52,7 @@ private const val QUOTE_HIGHLIGHT_HOLD_MILLIS = 700L private const val QUOTE_HIGHLIGHT_FADE_OUT_MILLIS = 1500 data class ChatMessageContext( + val currentlyPlayingVoiceMessageId: Int? = null, val isOneToOneConversation: Boolean = false, val conversationThreadId: Long? = null, val hasChatPermission: Boolean = true, @@ -175,17 +176,18 @@ fun ChatMessageView( ) } - is MessageTypeContent.Voice -> { - VoiceMessage( - typeContent = content, - message = message, - isOneToOneConversation = context.isOneToOneConversation, - conversationThreadId = context.conversationThreadId, - onPlayPauseClick = callbacks.onVoicePlayPauseClick, - onSeek = callbacks.onVoiceSeek, - onSpeedClick = callbacks.onVoiceSpeedClick - ) - } + is MessageTypeContent.Voice -> { + VoiceMessage( + typeContent = content, + message = message, + isOneToOneConversation = context.isOneToOneConversation, + conversationThreadId = context.conversationThreadId, + currentlyPlayingVoiceMessageId = context.currentlyPlayingVoiceMessageId, + onPlayPauseClick = callbacks.onVoicePlayPauseClick, + onSeek = callbacks.onVoiceSeek, + onSpeedClick = callbacks.onVoiceSpeedClick + ) + } is MessageTypeContent.Poll -> { PollMessage( diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt index bc9ab4e3bcf..8e765b56265 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt @@ -88,6 +88,7 @@ data class ChatViewState( val chatItems: List, val isOneToOneConversation: Boolean, val conversationThreadId: Long? = null, + val currentlyPlayingVoiceMessageId: Int? = null, val hasChatPermission: Boolean = true, val initialUnreadCount: Int = 0, val initialShowUnreadPopup: Boolean = false, @@ -346,10 +347,10 @@ fun ChatView( isSelected = state.highlightedMessageId == chatItem.uiMessage.id, highlightSearchTerm = state.highlightedSearchTerm, context = ChatMessageContext( + currentlyPlayingVoiceMessageId = state.currentlyPlayingVoiceMessageId, isOneToOneConversation = state.isOneToOneConversation, conversationThreadId = state.conversationThreadId, - hasChatPermission = state.hasChatPermission, - downloadingFileState = state.downloadingFileState + hasChatPermission = state.hasChatPermission ), callbacks = ChatMessageCallbacks( onLongClick = callbacks.messageCallbacks.onLongClick, diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt index be42be087e8..e25220c2ae6 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt @@ -54,6 +54,7 @@ fun VoiceMessage( message: ChatMessageUi, isOneToOneConversation: Boolean = false, conversationThreadId: Long? = null, + currentlyPlayingVoiceMessageId: Int? = null, onPlayPauseClick: (Int) -> Unit = {}, onSeek: (messageId: Int, progress: Int) -> Unit = { _, _ -> }, onSpeedClick: (messageId: Int) -> Unit = {} @@ -83,6 +84,12 @@ fun VoiceMessage( label = "size" ) + val icon = if (message.id != currentlyPlayingVoiceMessageId) { + Icons.Filled.PlayArrow + } else { + if (typeContent.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow + } + Column { Row( verticalAlignment = Alignment.CenterVertically, @@ -96,7 +103,7 @@ fun VoiceMessage( modifier = Modifier.size(48.dp) ) { Icon( - imageVector = if (typeContent.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + imageVector = icon, contentDescription = stringResource(R.string.play_pause_voice_message), modifier = Modifier.size(40.dp) )