Skip to content

Commit

Permalink
Added JumpToPlayingAudio Button
Browse files Browse the repository at this point in the history
  • Loading branch information
borichellow committed Dec 16, 2024
1 parent 49c749c commit b1862e8
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ internal constructor(
extraBufferCapacity = 1
)

private val audioSpeedFlow = MutableSharedFlow<AudioSpeed>(
private val _audioSpeed = MutableSharedFlow<AudioSpeed>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = 1,
replay = 1
Expand Down Expand Up @@ -191,7 +191,7 @@ internal constructor(
audioMessageStateHistory
}.onStart { emit(audioMessageStateHistory) }

val audioSpeed: Flow<AudioSpeed> = audioSpeedFlow.onStart { emit(AudioSpeed.NORMAL) }
val audioSpeed: Flow<AudioSpeed> = _audioSpeed.onStart { emit(AudioSpeed.NORMAL) }

private var currentAudioMessageId: String? = null

Expand Down Expand Up @@ -381,7 +381,7 @@ internal constructor(

private suspend fun updateSpeedFlow() {
val currentSpeed = AudioSpeed.fromFloat(audioMediaPlayer.playbackParams.speed)
audioSpeedFlow.emit(currentSpeed)
_audioSpeed.emit(currentSpeed)
}

internal fun close() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.FloatingActionButtonDefaults
Expand All @@ -65,6 +70,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.PagingData
Expand Down Expand Up @@ -137,6 +143,7 @@ import com.wire.android.ui.home.conversations.media.preview.ImagesPreviewNavBack
import com.wire.android.ui.home.conversations.messages.AudioMessagesState
import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel
import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState
import com.wire.android.ui.home.conversations.messages.PlayingAudiMessage
import com.wire.android.ui.home.conversations.messages.draft.MessageDraftViewModel
import com.wire.android.ui.home.conversations.messages.item.MessageClickActions
import com.wire.android.ui.home.conversations.messages.item.MessageContainerItem
Expand All @@ -160,7 +167,9 @@ import com.wire.android.ui.home.messagecomposer.state.rememberMessageComposerSta
import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectMessageDialog
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.DateAndTimeParsers
import com.wire.android.util.normalizeLink
import com.wire.android.util.serverDate
import com.wire.android.util.ui.PreviewMultipleThemes
Expand Down Expand Up @@ -888,6 +897,7 @@ private fun ConversationScreen(
selectedMessageId = conversationMessagesViewState.searchedMessageId,
messageComposerStateHolder = messageComposerStateHolder,
messages = conversationMessagesViewState.messages,
playingAudiMessage = conversationMessagesViewState.playingAudiMessage,
onSendMessage = onSendMessage,
onPingOptionClicked = onPingOptionClicked,
onImagesPicked = onImagesPicked,
Expand Down Expand Up @@ -994,6 +1004,7 @@ private fun ConversationScreenContent(
onNavigateToReplyOriginalMessage: (UIMessage) -> Unit,
openDrawingCanvas: () -> Unit,
currentTimeInMillisFlow: Flow<Long> = flow {},
playingAudiMessage: PlayingAudiMessage?,
) {
val lazyPagingMessages = messages.collectAsLazyPagingItems()

Expand Down Expand Up @@ -1033,7 +1044,8 @@ private fun ConversationScreenContent(
conversationDetailsData = conversationDetailsData,
selectedMessageId = selectedMessageId,
interactionAvailability = messageComposerStateHolder.messageComposerViewState.value.interactionAvailability,
currentTimeInMillisFlow = currentTimeInMillisFlow
currentTimeInMillisFlow = currentTimeInMillisFlow,
playingAudiMessage = playingAudiMessage
)
},
onChangeSelfDeletionClicked = onChangeSelfDeletionClicked,
Expand Down Expand Up @@ -1099,7 +1111,8 @@ fun MessageList(
interactionAvailability: InteractionAvailability,
clickActions: MessageClickActions.Content,
modifier: Modifier = Modifier,
currentTimeInMillisFlow: Flow<Long> = flow { }
currentTimeInMillisFlow: Flow<Long> = flow { },
playingAudiMessage: PlayingAudiMessage?
) {
val prevItemCount = remember { mutableStateOf(lazyPagingMessages.itemCount) }
val readLastMessageAtStartTriggered = remember { mutableStateOf(false) }
Expand Down Expand Up @@ -1227,6 +1240,11 @@ fun MessageList(
}
}
}
JumpToPlayingAudioButton(
lazyPagingMessages = lazyPagingMessages,
lazyListState = lazyListState,
playingAudiMessage = playingAudiMessage
)
JumpToLastMessageButton(lazyListState = lazyListState)
}
)
Expand Down Expand Up @@ -1361,6 +1379,62 @@ fun JumpToLastMessageButton(
}
}

@Composable
fun BoxScope.JumpToPlayingAudioButton(
lazyListState: LazyListState,
playingAudiMessage: PlayingAudiMessage?,
modifier: Modifier = Modifier,
lazyPagingMessages: LazyPagingItems<UIMessage>,
coroutineScope: CoroutineScope = rememberCoroutineScope()
) {
val indexOfPlayedMessage = playingAudiMessage?.let {
lazyPagingMessages.itemSnapshotList
.indexOfFirst { playingAudiMessage.messageId == it?.header?.messageId }
} ?: -1

if (indexOfPlayedMessage < 0) return

// todo cyka try to remember indexes
val visible = playingAudiMessage?.let {
val firstVisibleIndex = lazyListState.firstVisibleItemIndex
val lastVisibleIndex = firstVisibleIndex + lazyListState.layoutInfo.visibleItemsInfo.size
indexOfPlayedMessage in firstVisibleIndex..lastVisibleIndex
} ?: false

if (!visible) return

Row(
modifier = modifier
.align(Alignment.TopCenter)
.clickable { coroutineScope.launch { lazyListState.animateScrollToItem(indexOfPlayedMessage) } }
.padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x)
.background(
color = colorsScheme().secondaryText,
shape = RoundedCornerShape(MaterialTheme.wireDimensions.buttonCornerSize)
)
) {
Icon(
modifier = Modifier.weight(1f),
painter = painterResource(id = R.drawable.ic_play),
contentDescription = null,
tint = MaterialTheme.wireColorScheme.onPrimaryButtonEnabled
)
Spacer(Modifier.width(dimensions().spacing8x))
Text(
text = playingAudiMessage!!.authorName,
color = colorsScheme().onPrimaryButtonEnabled,
style = MaterialTheme.wireTypography.body04,
)
Spacer(Modifier.width(dimensions().spacing8x))
Text(
modifier = Modifier.weight(1f),
text = DateAndTimeParsers.audioMessageTime(playingAudiMessage.currentTimeMs.toLong()),
color = colorsScheme().onPrimaryButtonEnabled,
style = MaterialTheme.wireTypography.body04,
)
}
}

private fun CoroutineScope.withSmoothScreenLoad(block: () -> Unit) = launch {
val smoothAnimationDuration = 200.milliseconds
delay(smoothAnimationDuration) // we wait a bit until the whole screen is loaded to show the animation properly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.paging.PagingData
import androidx.paging.map
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.media.audiomessage.AudioMediaPlayingState
import com.wire.android.media.audiomessage.AudioSpeed
import com.wire.android.media.audiomessage.ConversationAudioMessagePlayerProvider
import com.wire.android.model.SnackBarMessage
Expand Down Expand Up @@ -67,7 +68,6 @@ import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePosit
import com.wire.kalium.logic.feature.message.ToggleReactionUseCase
import com.wire.kalium.logic.feature.sessionreset.ResetSessionResult
import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase
import com.wire.kalium.logic.functional.combine
import com.wire.kalium.logic.functional.onFailure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentMap
Expand All @@ -78,6 +78,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -186,12 +189,40 @@ class ConversationMessagesViewModel @Inject constructor(
}

private fun observeAudioPlayerState() {
val observableAudioMessagesState = conversationAudioMessagePlayer.observableAudioMessagesState
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)

val playingMessageData = observableAudioMessagesState
.map { audioMessageStates ->
audioMessageStates.firstNotNullOfOrNull { (messageId, audioState) ->
if (audioState.audioMediaPlayingState == AudioMediaPlayingState.Playing) messageId
else null
}
}.distinctUntilChanged()
.map { messageId -> messageId?.let { getMessageByIdUseCase(conversationId, it) } }
.filterIsInstance<GetMessageByIdUseCase.Result.Success?>()
.map { it?.message }

viewModelScope.launch {
conversationAudioMessagePlayer.observableAudioMessagesState
.combine(conversationAudioMessagePlayer.audioSpeed)
.collect { (audioMessageStates, audioSpeed) ->
combine(
observableAudioMessagesState,
conversationAudioMessagePlayer.audioSpeed,
playingMessageData
) { audioMessageStates, audioSpeed, playingMessage ->
val audioMessagesState = AudioMessagesState(audioMessageStates.toPersistentMap(), audioSpeed)
val playingAudiMessage = playingMessage?.let {
PlayingAudiMessage(
messageId = playingMessage.id,
authorName = playingMessage.sender?.name ?: "",
currentTimeMs = audioMessageStates[playingMessage.id]?.currentPositionInMs ?: 0
)
}
audioMessagesState to playingAudiMessage
}
.collect { (audioMessagesState, playingAudiMessage) ->
conversationViewState = conversationViewState.copy(
audioMessagesState = AudioMessagesState(audioMessageStates.toPersistentMap(), audioSpeed)
audioMessagesState = audioMessagesState,
playingAudiMessage = playingAudiMessage
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,21 @@ data class ConversationMessagesViewState(
val downloadedAssetDialogState: DownloadedAssetDialogVisibilityState = DownloadedAssetDialogVisibilityState.Hidden,
val audioMessagesState: AudioMessagesState = AudioMessagesState(),
val assetStatuses: PersistentMap<String, MessageAssetStatus> = persistentMapOf(),
val searchedMessageId: String? = null
val searchedMessageId: String? = null,
val playingAudiMessage: PlayingAudiMessage? = null
)

data class AudioMessagesState(
val audioStates: PersistentMap<String, AudioState> = persistentMapOf(),
val audioSpeed: AudioSpeed = AudioSpeed.NORMAL
)

data class PlayingAudiMessage(
val messageId: String,
val authorName: String,
val currentTimeMs: Int
)

sealed class DownloadedAssetDialogVisibilityState {
object Hidden : DownloadedAssetDialogVisibilityState()
data class Displayed(val assetData: AssetBundle, val messageId: String) : DownloadedAssetDialogVisibilityState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.DateAndTimeParsers
import com.wire.android.util.ui.PreviewMultipleThemes

@Composable
Expand Down Expand Up @@ -364,51 +365,19 @@ private fun getPlayOrPauseIcon(audioMediaPlayingState: AudioMediaPlayingState):
@Suppress("MagicNumber")
private fun getDefaultWaveMask(): List<Int> = List(75) { 1 }

// helper wrapper class to format the time that is left
// helper wrapper class to format the time
private data class AudioDuration(val totalDurationInMs: AudioState.TotalTimeInMs, val currentPositionInMs: Int) {
companion object {
const val totalMsInSec = 1000
const val totalSecInMin = 60
const val UNKNOWN_DURATION_LABEL = "-:--"
}

fun formattedTimeLeft(): String {
if (totalDurationInMs is AudioState.TotalTimeInMs.Known) {
val totalTimeInSec = totalDurationInMs.value / totalMsInSec
val currentPositionInSec = currentPositionInMs / totalMsInSec

val isTotalTimeInSecKnown = totalTimeInSec > 0

val timeLeft = if (!isTotalTimeInSecKnown) {
currentPositionInSec
} else {
totalTimeInSec - currentPositionInSec
}

return formattedTime(timeLeft)
}

return UNKNOWN_DURATION_LABEL
}

fun formattedCurrentTime(): String =
formattedTime(currentPositionInMs / totalMsInSec)
fun formattedCurrentTime(): String = DateAndTimeParsers.audioMessageTime(currentPositionInMs.toLong())

fun formattedTotalTime(): String = if (totalDurationInMs is AudioState.TotalTimeInMs.Known) {
formattedTime(totalDurationInMs.value / totalMsInSec)
DateAndTimeParsers.audioMessageTime(totalDurationInMs.value.toLong())
} else {
UNKNOWN_DURATION_LABEL
}

private fun formattedTime(timeSeconds: Int): String {
// sanity check, timeLeft, should not be smaller, however if the back-end makes mistake we
// will display a negative values, which we do not want
val minutes = if (timeSeconds < 0) 0 else timeSeconds / totalSecInMin
val seconds = if (timeSeconds < 0) 0 else timeSeconds % totalSecInMin
val formattedSeconds = String.format("%02d", seconds)

return "$minutes:$formattedSeconds"
}
}

@PreviewMultipleThemes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class DateAndTimeParsers private constructor() {
private val messageTimeFormatter = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT, Locale.getDefault()).apply {
this.timeZone = java.util.TimeZone.getDefault()
}
private val audioMessageTimeFormat = DateTimeFormatter.ofPattern("mm:ss", Locale.getDefault())
.withZone(ZoneId.systemDefault())

@Deprecated("Date String parsing is discouraged and will be removed soon for direct Instant/DateTime versions")
fun serverDate(stringDate: String): Date? {
Expand Down Expand Up @@ -137,5 +139,7 @@ class DateAndTimeParsers private constructor() {
} catch (e: Exception) {
null
}

fun audioMessageTime(timeMs: Long): String = audioMessageTimeFormat.format(java.time.Instant.ofEpochMilli(timeMs))
}
}

0 comments on commit b1862e8

Please sign in to comment.