This commit is contained in:
Eric85710 2026-06-16 16:30:34 +08:00
parent 944d91e3d9
commit e98eb1706d
3 changed files with 73 additions and 59 deletions

View file

@ -112,8 +112,18 @@ data class Message(
// 🌟 核心新增:讓 UI 可以辨識訊息發送狀態! // 🌟 核心新增:讓 UI 可以辨識訊息發送狀態!
@kotlin.jvm.Transient // 👈 告訴 Moshi 這個欄位不需要解析 JSON,這是我們本地維護的 @kotlin.jvm.Transient // 👈 告訴 Moshi 這個欄位不需要解析 JSON,這是我們本地維護的
val status: MessageStatus = MessageStatus.SUCCESS val status: MessageStatus = MessageStatus.SUCCESS,
)
@kotlin.jvm.Transient
var callLogInfo: CallLogInfo? = null
){
init {
if (type == "system" || type.startsWith("call")) {
callLogInfo = CallLogInfo.parseContent(content)
}
}
}
//send message //send message
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -134,26 +144,24 @@ data class MessageReactionUsersResponse(
//voice call //voice call
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CallLogInfo( data class CallLogInfo(
@Json(name = "type") val type: String, // 例如: call, call_log, call_missed, call_rejected 等 @Json(name = "type") val type: String, // 例如: call_missed, call_rejected
@Json(name = "initiator_id") val initiator_id: String, @Json(name = "initiator_id") val initiatorId: String, // 順便改成 Kotlin 的駝峰命名規範
@Json(name = "has_video") val has_video: Boolean @Json(name = "has_video") val hasVideo: Boolean
) { ) {
companion object { companion object {
private val moshi = Moshi.Builder() private val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory()) .addLast(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory())
.build() .build()
private val adapter = moshi.adapter(CallLogInfo::class.java) private val adapter = moshi.adapter(CallLogInfo::class.java)
fun parse(message: Message): CallLogInfo? { // 🌟 改成只單純解析 String
// 🌟 修正:只要是 call 開頭的 type 都允許解析,避免 call_missed 被擋掉 fun parseContent(content: String?): CallLogInfo? {
if (message.type == null || !message.type.startsWith("call")) return null val json = content?.trim() ?: return null
if (!json.startsWith("{") || !json.endsWith("}")) return null
val content = message.content?.trim() ?: return null
if (!content.startsWith("{") || !content.endsWith("}")) return null
return try { return try {
adapter.fromJson(content) adapter.fromJson(json)
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("CallLogInfo", "解析通話 JSON 失敗: ${e.message}") android.util.Log.e("CallLogInfo", "解析通話 JSON 失敗: ${e.message}")
null null

View file

@ -8,12 +8,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Call import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.CallMissed import androidx.compose.material.icons.filled.CallMissed
import androidx.compose.material.icons.filled.VideoCall import androidx.compose.material.icons.filled.VideoCall
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -27,13 +29,13 @@ import com.example.login_v3.data.api.api_class.CallLogInfo
@Composable @Composable
fun CallLogBubble( fun CallLogBubble(
callLog: CallLogInfo, callLog: CallLogInfo,
isMe: Boolean, isMe: Boolean, // 這一則訊息是不是「我」發送的
currentUserId: String, currentUserId: String,
partnerDisplayName: String, partnerDisplayName: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// 判斷這通電話是不是「我」撥打的 // 🎯 改造 1:配合方案 A 的變數名稱修正 (initiator_id -> initiatorId)
val isIInitiated = callLog.initiator_id == currentUserId val isIInitiated = callLog.initiatorId == currentUserId
// 根據通話狀態決定圖示、顏色與顯示主標題 // 根據通話狀態決定圖示、顏色與顯示主標題
val (icon, iconColor, titleText) = when (callLog.type) { val (icon, iconColor, titleText) = when (callLog.type) {
@ -45,61 +47,66 @@ fun CallLogBubble(
) )
} }
else -> { else -> {
val callTypeStr = if (callLog.has_video) "視訊通話" else "語音通話" // 🎯 改造 2:配合方案 A 的變數名稱修正 (has_video -> hasVideo)
val callTypeStr = if (callLog.hasVideo) "視訊通話" else "語音通話"
Triple( Triple(
if (callLog.has_video) Icons.Default.VideoCall else Icons.Default.Call, if (callLog.hasVideo) Icons.Default.VideoCall else Icons.Default.Call,
if (isMe) Color(0xFF4CAF50) else Color(0xFF1976D2), if (isMe) Color(0xFF4CAF50) else Color(0xFF1976D2),
if (isIInitiated) "撥出$callTypeStr" else "已接聽$callTypeStr" if (isIInitiated) "撥出$callTypeStr" else "已接聽$callTypeStr"
) )
} }
} }
// 🌟 新增:來電顯示副標題邏輯 // 來電顯示副標題邏輯
val callTypeLabel = if (callLog.has_video) "視訊" else "語音" val callTypeLabel = if (callLog.hasVideo) "視訊" else "語音"
val subText = when { val subText = when {
// 我撥出去的 -> 顯示「撥給 某某」
isIInitiated -> "$callTypeLabel • 撥給 $partnerDisplayName" isIInitiated -> "$callTypeLabel • 撥給 $partnerDisplayName"
// 對方撥進來的 -> 顯示「來自 某某」
else -> "$callTypeLabel • 來自 $partnerDisplayName" else -> "$callTypeLabel • 來自 $partnerDisplayName"
} }
Row( // 🎯 改造 3:外層加上氣泡背景與剪裁,讓它看起來更像聊天氣泡
modifier = modifier.padding(vertical = 2.dp), Surface(
verticalAlignment = Alignment.CenterVertically, modifier = modifier.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp) shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) // 給予淡淡的灰色氣泡背景
) { ) {
// 左側:圓形背景與狀態圖示 Row(
Box( modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
modifier = Modifier verticalAlignment = Alignment.CenterVertically,
.size(38.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
.background(iconColor.copy(alpha = 0.12f), shape = CircleShape),
contentAlignment = Alignment.Center
) { ) {
Icon( // 左側:圓形背景與狀態圖示
imageVector = icon, Box(
contentDescription = titleText, modifier = Modifier
tint = iconColor, .size(38.dp)
modifier = Modifier.size(22.dp) .background(iconColor.copy(alpha = 0.12f), shape = CircleShape),
) contentAlignment = Alignment.Center
} ) {
Icon(
imageVector = icon,
contentDescription = titleText,
tint = iconColor,
modifier = Modifier.size(22.dp)
)
}
// 右側:通話狀態與來電顯示文字 // 右側:通話狀態與來電顯示文字
Column { Column {
Text( Text(
text = titleText, text = titleText,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
)
) )
) Text(
Text( text = subText,
// 🌟 改用動態組合的來電顯示文字 style = MaterialTheme.typography.bodySmall.copy(
text = subText, fontSize = 12.sp,
style = MaterialTheme.typography.bodySmall.copy( color = Color.Gray
fontSize = 12.sp, )
color = Color.Gray
) )
) }
} }
} }
} }

View file

@ -517,7 +517,7 @@ fun MessageRow(
val isImage = isAttachmentImage || isLegacyImage val isImage = isAttachmentImage || isLegacyImage
val rawContent = message.content ?: "" val rawContent = message.content ?: ""
val callLog = remember(message.id, message.content) { CallLogInfo.parse(message) } val callLog = message.callLogInfo
val finalImageModel = remember(message) { val finalImageModel = remember(message) {
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullOrBlank()) { val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullOrBlank()) {
@ -625,17 +625,16 @@ fun MessageRow(
) )
} }
} else if (callLog != null) { } else if (callLog != null) {
// 🌟 通話紀錄渲染 // 🌟 🎯 改造 2:通話紀錄渲染優化
// 移除原本包裹在外層、帶有背景與 Padding 的 Box,改由長按/點擊事件的容器包住 CallLogBubble
Box( Box(
modifier = Modifier modifier = Modifier
.widthIn(max = 280.dp) .widthIn(max = 280.dp)
.clip(RoundedCornerShape(12.dp)) // 讓通話紀錄一樣能響應點擊與長按(如刪除、回覆)
.combinedClickable( .combinedClickable(
onLongClick = { onReplyClick(message) }, onLongClick = { onReplyClick(message) },
onClick = { onRowClick(message) } onClick = { onRowClick(message) }
) )
.background(color = finalBubbleColor)
.padding(horizontal = 14.dp, vertical = 10.dp)
) { ) {
CallLogBubble( CallLogBubble(
callLog = callLog, callLog = callLog,