diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index 7525e83d..9d1cd257 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt @@ -37,6 +37,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { val textColor = if (message.isOutgoing) Color.White else Color.Black val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp + val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp val isVoice = message.contentType == "audio/opus" val isMedia = message.contentType.isNotEmpty() && !isVoice && message.contentType != "text/plain" @@ -47,7 +48,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { ) { Column( modifier = Modifier - .widthIn(min = minBubbleWidth, max = 300.dp) + .widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) .clip(RoundedCornerShape(12.dp)) .background(bgColor) .padding(12.dp) @@ -65,40 +66,51 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) { } } - Spacer(Modifier.height(2.dp)) - Text( - formatTime(message.ts), - fontSize = 11.sp, - color = textColor.copy(alpha = 0.5f), - modifier = Modifier.align(Alignment.End) - ) + if (!isVoice) { + Spacer(Modifier.height(2.dp)) + Text( + formatTime(message.ts), + fontSize = 11.sp, + color = textColor.copy(alpha = 0.5f), + modifier = Modifier.align(Alignment.End) + ) + } } } } @Composable private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) -> Unit) { - val durationSec = message.voiceDurationMs / 1000f val wf = message.waveform 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) { - /* Play button */ - Box( - modifier = Modifier.size(32.dp).background(Color(0xFF3390EC), CircleShape) - .clickable { if (message.filePath.isNotEmpty()) onPlay(message.filePath) }, - contentAlignment = Alignment.Center - ) { - Text("\u25B6", fontSize = 14.sp, color = Color.White) - } - Spacer(Modifier.width(8.dp)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + /* Play/Pause button */ + Box( + modifier = Modifier.size(32.dp).background(Color(0xFF3390EC), CircleShape) + .clickable { + if (!playing && message.filePath.isNotEmpty()) { + playing = true; elapsedMs = 0; onPlay(message.filePath) + } else { + playing = false + } + }, + contentAlignment = Alignment.Center + ) { + Text(if (playing) "\u23F8" else "\u25B6", fontSize = 14.sp, color = Color.White) + } + Spacer(Modifier.width(8.dp)) - /* Waveform */ - if (wfCount >= 2) { - Box(modifier = Modifier.weight(1f).height(32.dp)) { - Canvas(modifier = Modifier.fillMaxSize()) { + /* Waveform */ + if (wfCount >= 2) { + Canvas(modifier = Modifier.weight(1f).height(28.dp)) { val barW = size.width / wfCount - val ph = size.height.toFloat() + val ph = size.height for (i in 0 until wfCount) { val v = wf[i] 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 y = ph - barH drawRect( - color = Color(0xFF3390EC).copy(alpha = 0.5f), + color = Color(0xFF3390EC).copy(alpha = 0.35f), topLeft = Offset(x, y), - size = Size(barW * 0.8f, barH) + size = Size(barW * 0.75f, barH) ) } } } - Spacer(Modifier.width(4.dp)) } - /* Duration */ - Text( - formatDurationMs(message.voiceDurationMs), - fontSize = 13.sp, - color = textColor.copy(alpha = 0.7f), - modifier = Modifier.padding(start = 4.dp) - ) + Spacer(Modifier.height(4.dp)) + + /* Duration + timestamp */ + Row( + modifier = Modifier.fillMaxWidth(), + 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) + ) + } } } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index e25d80cf..7e10ea74 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -377,10 +377,10 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) { int dlen = sqlite3_column_bytes(st, 2); const char* lattr = (const char*)sqlite3_column_text(st, 4); - char txt[256] = "", ct[64] = "text/plain"; + char txt[2048] = "", ct[64] = "text/plain"; if (dptr && dlen > 0) { const char* ds = strstr((const char*)dptr, "\"d\":\""); - if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 250) tl = 250; memcpy(txt, ds, tl); txt[tl] = '\0'; } } + if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 2000) tl = 2000; memcpy(txt, ds, tl); txt[tl] = '\0'; } } const char* cts = strstr((const char*)dptr, "\"ct\":\""); if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } } }