Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DrawableUtils
import com.nextcloud.talk.ui.PlaybackSpeed
import java.time.LocalDate

// immutable class for chat message UI. only val, no vars!
Expand Down
95 changes: 95 additions & 0 deletions app/src/main/java/com/nextcloud/talk/ui/ComposeWaveformSeekbar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.nextcloud.talk.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

const val WAVEFORM_THUMB_SIZE = 20
const val WAVEFORM_SIZE = 30
const val OVERLAP = 0.025
Comment thread
rapterjet2004 marked this conversation as resolved.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ComposeWaveformSeekBar(value: Float, onValueChange: (Float) -> Unit, modifier: Modifier, waveData: FloatArray) {
val barWidth = Stroke.DefaultMiter
val thumbSize = WAVEFORM_THUMB_SIZE.dp
val inversePrimary = MaterialTheme.colorScheme.inversePrimary
val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer

Slider(
value = value,
onValueChange = onValueChange,
track = {
Box(
modifier = modifier
.drawWithCache {
onDrawBehind {
val height = this.size.height
val width = this.size.width
val midpoint = (this.size.height / 2f)

val barGap = (width - waveData.size * barWidth) / (waveData.size - 1).toFloat() + 1
for (i in waveData.indices) {
val x: Float = i * (barWidth + barGap)
val y: Float = waveData[i] * height
Comment thread
rapterjet2004 marked this conversation as resolved.
val isXBeforeThumb = (x / this.size.width) <= value + OVERLAP

drawLine(
if (isXBeforeThumb) inversePrimary else onPrimaryContainer,
start = Offset(x, midpoint - y),
end = Offset(x, midpoint + y),
strokeWidth = Stroke.DefaultMiter,
Comment thread
rapterjet2004 marked this conversation as resolved.
cap = StrokeCap.Round
)
}
}
}
)
},
thumb = {
Box(
modifier = Modifier
.size(thumbSize)
.background(inversePrimary, shape = CircleShape)
)
}
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun Preview() {
val waveData = remember { FloatArray(WAVEFORM_SIZE) { (Math.random() % 1).toFloat() } }

ComposeWaveformSeekBar(
0f,
{},
modifier = Modifier
.height(MAX_HEIGHT.dp)
.fillMaxWidth(),
waveData
Comment thread
rapterjet2004 marked this conversation as resolved.
)
}
84 changes: 37 additions & 47 deletions app/src/main/java/com/nextcloud/talk/ui/chat/VoiceMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
package com.nextcloud.talk.ui.chat

import android.text.format.DateUtils
import android.widget.SeekBar
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -19,26 +19,34 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.nextcloud.talk.R
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
import com.nextcloud.talk.ui.WaveformSeekBar
import com.nextcloud.talk.ui.ComposeWaveformSeekBar
import com.nextcloud.talk.ui.WAVEFORM_SIZE
import com.nextcloud.talk.utils.AudioUtils

private const val SEEKBAR_MAX = 100
private const val START_WAVE_FORM_HEIGHT = 10
private const val END_WAVE_FORM_HEIGHT = 56

@OptIn(ExperimentalMaterial3Api::class)
@Suppress("Detekt.LongMethod", "LongParameterList")
@Composable
fun VoiceMessage(
Expand All @@ -57,12 +65,23 @@ fun VoiceMessage(
forceTimeBelow = true,
content = {
val inversePrimaryColor = colorScheme.inversePrimary
val inversePrimary = remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
remember(inversePrimaryColor) { inversePrimaryColor.toArgb() }
val onPrimaryContainerColor = colorScheme.onPrimaryContainer
val onPrimaryContainer = remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
remember(onPrimaryContainerColor) { onPrimaryContainerColor.toArgb() }
Comment thread
rapterjet2004 marked this conversation as resolved.
val remainingSeconds = (typeContent.durationSeconds - typeContent.playedSeconds).coerceAtLeast(0)
val waveformData = remember(typeContent.waveform) { typeContent.waveform.toFloatArray() }
val lastWaveformData = remember { mutableListOf<Float>() }
val waveformData = remember(typeContent.waveform) {
val floatArr = typeContent.waveform.toFloatArray()
if (floatArr.size < WAVEFORM_SIZE) {
FloatArray(WAVEFORM_SIZE)
} else {
AudioUtils.shrinkFloatArray(floatArr, WAVEFORM_SIZE)
}
}

val animValue by animateIntAsState(
if (waveformData.average() > 0) END_WAVE_FORM_HEIGHT else START_WAVE_FORM_HEIGHT,
label = "size"
)

Column {
Row(
Expand All @@ -84,49 +103,20 @@ fun VoiceMessage(
}
}

AndroidView(
factory = { ctx ->
WaveformSeekBar(ctx).apply {
max = SEEKBAR_MAX
setWaveData(waveformData)
setColors(
inversePrimary,
onPrimaryContainer
)
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar?,
progress: Int,
fromUser: Boolean
) {
if (fromUser) {
onSeek(message.id, progress)
}
}
var sliderValue by remember { mutableFloatStateOf(0f) }
sliderValue = typeContent.seekbarProgress * 1f / SEEKBAR_MAX

Comment thread
rapterjet2004 marked this conversation as resolved.
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit

override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
})
}
},
update = { seekBar ->
seekBar.max = SEEKBAR_MAX
val waveformChanged = typeContent.waveform != lastWaveformData
if (waveformChanged) {
lastWaveformData.clear()
lastWaveformData.addAll(typeContent.waveform)
seekBar.setWaveData(waveformData)
seekBar.requestLayout()
}
seekBar.setColors(inversePrimary, onPrimaryContainer)
seekBar.progress = typeContent.seekbarProgress
seekBar.isEnabled = !typeContent.isDownloading
seekBar.invalidate()
ComposeWaveformSeekBar(
sliderValue,
{
val progressI = (it * SEEKBAR_MAX).toInt()
onSeek(message.id, progressI)
},
Comment thread
rapterjet2004 marked this conversation as resolved.
modifier = Modifier
.weight(1f)
.height(56.dp)
.height(animValue.dp)
.fillMaxWidth()
.padding(8.dp), // or weight(1f),
waveformData
Comment thread
rapterjet2004 marked this conversation as resolved.
)

TextButton(
Expand Down
Loading