|
|
|
@ -37,6 +37,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { |
|
|
|
val textColor = if (message.isOutgoing) Color.White else Color.Black |
|
|
|
val textColor = if (message.isOutgoing) Color.White else Color.Black |
|
|
|
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start |
|
|
|
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start |
|
|
|
val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp |
|
|
|
val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp |
|
|
|
|
|
|
|
val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp |
|
|
|
|
|
|
|
|
|
|
|
val isVoice = message.contentType == "audio/opus" |
|
|
|
val isVoice = message.contentType == "audio/opus" |
|
|
|
val isMedia = message.contentType.isNotEmpty() && !isVoice && message.contentType != "text/plain" |
|
|
|
val isMedia = message.contentType.isNotEmpty() && !isVoice && message.contentType != "text/plain" |
|
|
|
@ -47,7 +48,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { |
|
|
|
) { |
|
|
|
) { |
|
|
|
Column( |
|
|
|
Column( |
|
|
|
modifier = Modifier |
|
|
|
modifier = Modifier |
|
|
|
.widthIn(min = minBubbleWidth, max = 300.dp) |
|
|
|
.widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) |
|
|
|
.clip(RoundedCornerShape(12.dp)) |
|
|
|
.clip(RoundedCornerShape(12.dp)) |
|
|
|
.background(bgColor) |
|
|
|
.background(bgColor) |
|
|
|
.padding(12.dp) |
|
|
|
.padding(12.dp) |
|
|
|
@ -65,40 +66,51 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Spacer(Modifier.height(2.dp)) |
|
|
|
if (!isVoice) { |
|
|
|
Text( |
|
|
|
Spacer(Modifier.height(2.dp)) |
|
|
|
formatTime(message.ts), |
|
|
|
Text( |
|
|
|
fontSize = 11.sp, |
|
|
|
formatTime(message.ts), |
|
|
|
color = textColor.copy(alpha = 0.5f), |
|
|
|
fontSize = 11.sp, |
|
|
|
modifier = Modifier.align(Alignment.End) |
|
|
|
color = textColor.copy(alpha = 0.5f), |
|
|
|
) |
|
|
|
modifier = Modifier.align(Alignment.End) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
@Composable |
|
|
|
private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) -> Unit) { |
|
|
|
private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) -> Unit) { |
|
|
|
val durationSec = message.voiceDurationMs / 1000f |
|
|
|
|
|
|
|
val wf = message.waveform |
|
|
|
val wf = message.waveform |
|
|
|
val wfCount = wf.size |
|
|
|
val wfCount = wf.size |
|
|
|
|
|
|
|
val durMs = message.voiceDurationMs |
|
|
|
|
|
|
|
val durSec = durMs / 1000f |
|
|
|
|
|
|
|
var playing by remember { mutableStateOf(false) } |
|
|
|
|
|
|
|
var elapsedMs by remember { mutableStateOf(0) } |
|
|
|
|
|
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
Column { |
|
|
|
/* Play button */ |
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
Box( |
|
|
|
/* Play/Pause button */ |
|
|
|
modifier = Modifier.size(32.dp).background(Color(0xFF3390EC), CircleShape) |
|
|
|
Box( |
|
|
|
.clickable { if (message.filePath.isNotEmpty()) onPlay(message.filePath) }, |
|
|
|
modifier = Modifier.size(32.dp).background(Color(0xFF3390EC), CircleShape) |
|
|
|
contentAlignment = Alignment.Center |
|
|
|
.clickable { |
|
|
|
) { |
|
|
|
if (!playing && message.filePath.isNotEmpty()) { |
|
|
|
Text("\u25B6", fontSize = 14.sp, color = Color.White) |
|
|
|
playing = true; elapsedMs = 0; onPlay(message.filePath) |
|
|
|
} |
|
|
|
} else { |
|
|
|
Spacer(Modifier.width(8.dp)) |
|
|
|
playing = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
contentAlignment = Alignment.Center |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
Text(if (playing) "\u23F8" else "\u25B6", fontSize = 14.sp, color = Color.White) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Spacer(Modifier.width(8.dp)) |
|
|
|
|
|
|
|
|
|
|
|
/* Waveform */ |
|
|
|
/* Waveform */ |
|
|
|
if (wfCount >= 2) { |
|
|
|
if (wfCount >= 2) { |
|
|
|
Box(modifier = Modifier.weight(1f).height(32.dp)) { |
|
|
|
Canvas(modifier = Modifier.weight(1f).height(28.dp)) { |
|
|
|
Canvas(modifier = Modifier.fillMaxSize()) { |
|
|
|
|
|
|
|
val barW = size.width / wfCount |
|
|
|
val barW = size.width / wfCount |
|
|
|
val ph = size.height.toFloat() |
|
|
|
val ph = size.height |
|
|
|
for (i in 0 until wfCount) { |
|
|
|
for (i in 0 until wfCount) { |
|
|
|
val v = wf[i] |
|
|
|
val v = wf[i] |
|
|
|
val db = if (v > 0.00001f) 20f * kotlin.math.log10(v.toDouble()).toFloat() else -100f |
|
|
|
val db = if (v > 0.00001f) 20f * kotlin.math.log10(v.toDouble()).toFloat() else -100f |
|
|
|
@ -107,23 +119,35 @@ private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) -> |
|
|
|
val x = i.toFloat() * barW |
|
|
|
val x = i.toFloat() * barW |
|
|
|
val y = ph - barH |
|
|
|
val y = ph - barH |
|
|
|
drawRect( |
|
|
|
drawRect( |
|
|
|
color = Color(0xFF3390EC).copy(alpha = 0.5f), |
|
|
|
color = Color(0xFF3390EC).copy(alpha = 0.35f), |
|
|
|
topLeft = Offset(x, y), |
|
|
|
topLeft = Offset(x, y), |
|
|
|
size = Size(barW * 0.8f, barH) |
|
|
|
size = Size(barW * 0.75f, barH) |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Spacer(Modifier.width(4.dp)) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Duration */ |
|
|
|
Spacer(Modifier.height(4.dp)) |
|
|
|
Text( |
|
|
|
|
|
|
|
formatDurationMs(message.voiceDurationMs), |
|
|
|
/* Duration + timestamp */ |
|
|
|
fontSize = 13.sp, |
|
|
|
Row( |
|
|
|
color = textColor.copy(alpha = 0.7f), |
|
|
|
modifier = Modifier.fillMaxWidth(), |
|
|
|
modifier = Modifier.padding(start = 4.dp) |
|
|
|
horizontalArrangement = Arrangement.SpaceBetween, |
|
|
|
) |
|
|
|
verticalAlignment = Alignment.Bottom |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
Text( |
|
|
|
|
|
|
|
formatDurationMs(durMs), |
|
|
|
|
|
|
|
fontSize = 12.sp, |
|
|
|
|
|
|
|
color = textColor.copy(alpha = 0.55f) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
Text( |
|
|
|
|
|
|
|
formatTime(message.ts), |
|
|
|
|
|
|
|
fontSize = 11.sp, |
|
|
|
|
|
|
|
color = textColor.copy(alpha = 0.4f), |
|
|
|
|
|
|
|
modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|