add delta sync for dm-v18
This commit is contained in:
parent
d84ba852c6
commit
3f0bbd8008
7 changed files with 190 additions and 5 deletions
|
|
@ -167,4 +167,12 @@ interface TecnologiaApi {
|
|||
@Path("room_id") roomId: String,
|
||||
@Path("msg_id") msgId: String
|
||||
): Response<Unit>
|
||||
|
||||
//edit message
|
||||
@PATCH("api/dm/{room_id}/messages/{msg_id}")
|
||||
suspend fun editMessage(
|
||||
@Path("room_id") roomId: String,
|
||||
@Path("msg_id") msgId: String,
|
||||
@Body request: EditMessageRequest
|
||||
): Response<EditMessageResponse>
|
||||
}
|
||||
|
|
@ -218,6 +218,17 @@ data class MessageReactionUsersResponse(
|
|||
@Json(name = "users") val users: List<String>
|
||||
)
|
||||
|
||||
//edit message
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EditMessageRequest(
|
||||
@Json(name = "content") val content: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EditMessageResponse(
|
||||
@Json(name = "ok") val ok: Boolean
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.provider.OpenableColumns
|
|||
import android.util.Log
|
||||
import com.example.login_v3.data.api.TecnologiaApi
|
||||
import com.example.login_v3.data.api.api_class.ChatRoom
|
||||
import com.example.login_v3.data.api.api_class.EditMessageRequest
|
||||
import com.example.login_v3.data.api.api_class.Message
|
||||
import com.example.login_v3.data.api.api_class.MessageResponse
|
||||
import com.example.login_v3.data.api.api_class.Reaction
|
||||
|
|
@ -569,6 +570,45 @@ class ChatRoomsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// 編輯訊息(樂觀更新版本)
|
||||
suspend fun editMessage(roomId: String, messageId: String, content: String): Result<Unit> {
|
||||
// 1. 先從本地撈出原本的訊息作為備份
|
||||
val backupEntity = try {
|
||||
messageDao.getMessageById(messageId)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
// 2. 樂觀更新:立刻在本地修改內容
|
||||
if (backupEntity != null) {
|
||||
try {
|
||||
val updatedEntity = backupEntity.copy(
|
||||
content = content,
|
||||
isEdited = true
|
||||
)
|
||||
messageDao.insertOrUpdate(updatedEntity)
|
||||
} catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 呼叫 API
|
||||
return try {
|
||||
val response = api.editMessage(roomId, messageId, EditMessageRequest(content))
|
||||
if (response.isSuccessful && response.body()?.ok == true) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
// 失敗則回滾
|
||||
backupEntity?.let { messageDao.insertOrUpdate(it) }
|
||||
Result.failure(Exception("編輯失敗"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 異常則回滾
|
||||
backupEntity?.let { messageDao.insertOrUpdate(it) }
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//download file
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Close
|
|||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Reply
|
||||
import androidx.compose.material.icons.filled.Edit // 🎯 新增
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -41,9 +42,11 @@ fun MessageActionMenuRow(
|
|||
onCancel: () -> Unit,
|
||||
onReplyClick: (MessageUiModel) -> Unit, // 👈 修正:這裡只需要定義型態
|
||||
onDeleteClick: (MessageUiModel) -> Unit,
|
||||
onEditClick: (MessageUiModel) -> Unit, // 🎯 新增
|
||||
isOwnMessage: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// ... (rest of the function)
|
||||
// 💡 取得系統剪貼簿,用來實作「複製文字」功能
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
|
|
@ -116,7 +119,16 @@ fun MessageActionMenuRow(
|
|||
}
|
||||
)
|
||||
|
||||
// 3. 刪除:只有在自己的訊息時才顯示 👈
|
||||
// 3. 編輯:只有在自己的文字訊息時顯示 🎯
|
||||
if (isOwnMessage && !isImage) {
|
||||
ChatMenuButton(
|
||||
imageVector = Icons.Default.Edit,
|
||||
label = "編輯",
|
||||
onClick = { onEditClick(message) }
|
||||
)
|
||||
}
|
||||
|
||||
// 4. 刪除:只有在自己的訊息時才顯示 👈
|
||||
if (isOwnMessage) {
|
||||
ChatMenuButton(
|
||||
imageVector = Icons.Default.Delete,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Photo
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
|
|
@ -28,6 +29,7 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect // 🎯 新增
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -44,12 +46,25 @@ import com.example.login_v3.home.Message.UI.Detail.MessageUiModel
|
|||
fun MessageInputBar(
|
||||
isLoading: Boolean,
|
||||
replyingMessage: MessageUiModel?,
|
||||
editingMessage: MessageUiModel? = null, // 🎯 新增
|
||||
onCancelReply: () -> Unit,
|
||||
onCancelEdit: () -> Unit = {}, // 🎯 新增
|
||||
onSendClick: (String) -> Unit,
|
||||
onEditSaveClick: (String) -> Unit = {}, // 🎯 新增
|
||||
onAttachmentClick: () -> Unit
|
||||
) {
|
||||
var textState by remember { mutableStateOf("") }
|
||||
|
||||
// 🎯 當進入編輯模式時,自動填入原本的內容
|
||||
LaunchedEffect(editingMessage) {
|
||||
if (editingMessage != null) {
|
||||
textState = editingMessage.content
|
||||
} else {
|
||||
// 如果從編輯模式退出且不是因為發送成功,可能需要清空?
|
||||
// 但通常 clearEditingMessage 會觸發這裡
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 外層包裹一層 Box,並加上 padding 與 navigationBarsPadding,確保懸浮且不被系統列擋住
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -103,6 +118,39 @@ fun MessageInputBar(
|
|||
}
|
||||
}
|
||||
|
||||
// 🎯 1.5 編輯預覽列
|
||||
AnimatedVisibility(visible = editingMessage != null) {
|
||||
if (editingMessage != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFFFF3E0)) // 編輯模式用淡橘色區分
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "編輯訊息",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color(0xFFE65100),
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
onCancelEdit()
|
||||
textState = "" // 取消編輯時清空
|
||||
}, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "取消編輯",
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 實際的輸入工具列
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -129,7 +177,10 @@ fun MessageInputBar(
|
|||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
decorationBox = { innerTextField ->
|
||||
if (textState.isEmpty()) {
|
||||
Text("請輸入訊息...", color = Color.LightGray)
|
||||
Text(
|
||||
text = if (editingMessage != null) "編輯訊息..." else "請輸入訊息...",
|
||||
color = Color.LightGray
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
|
|
@ -139,7 +190,11 @@ fun MessageInputBar(
|
|||
IconButton(
|
||||
onClick = {
|
||||
if (textState.isNotBlank() && !isLoading) {
|
||||
onSendClick(textState)
|
||||
if (editingMessage != null) {
|
||||
onEditSaveClick(textState)
|
||||
} else {
|
||||
onSendClick(textState)
|
||||
}
|
||||
textState = ""
|
||||
}
|
||||
},
|
||||
|
|
@ -150,9 +205,10 @@ fun MessageInputBar(
|
|||
} else {
|
||||
// 加上小背景讓送出按鈕更有重點
|
||||
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
|
||||
val icon = if (editingMessage != null) Icons.Default.Check else Icons.Default.Send
|
||||
Icon(
|
||||
imageVector = Icons.Default.Send,
|
||||
contentDescription = "送出",
|
||||
imageVector = icon,
|
||||
contentDescription = if (editingMessage != null) "儲存編輯" else "送出",
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ sealed interface BottomBarState {
|
|||
data class ActionMenu(val message: MessageUiModel) : BottomBarState // 長按:動作選單
|
||||
data class EmojiMenu(val message: MessageUiModel) : BottomBarState // 單擊:Emoji 工具列
|
||||
object AttachmentMenu : BottomBarState // 🎯 新增:附件選單
|
||||
data class Edit(val message: MessageUiModel) : BottomBarState // 🎯 新增:編輯模式
|
||||
}
|
||||
|
||||
// 🎯 新增:Top Bar 狀態機
|
||||
|
|
@ -155,6 +156,8 @@ fun MessageMessaging(
|
|||
// 🎯 新增:Top Bar 的本地狀態
|
||||
var topBarState by remember { mutableStateOf<TopBarState>(TopBarState.Regular) }
|
||||
|
||||
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
|
||||
|
||||
// 🌟 核心:使用 derivedStateOf 統一控管狀態權限(長按優先權通常大於單擊)
|
||||
// 🎯 新增:用來控管非訊息觸發的底部狀態(例如:AttachmentMenu)
|
||||
var localBottomBarState by remember { mutableStateOf<BottomBarState?>(null) }
|
||||
|
|
@ -163,6 +166,7 @@ fun MessageMessaging(
|
|||
when {
|
||||
actionMessage != null -> BottomBarState.ActionMenu(actionMessage!!)
|
||||
emojiTargetMessage != null -> BottomBarState.EmojiMenu(emojiTargetMessage!!)
|
||||
editingMessage != null -> BottomBarState.Edit(editingMessage!!) // 🎯 編輯模式優先
|
||||
localBottomBarState != null -> localBottomBarState!! // 🎯 如果有本地狀態,則採用它
|
||||
else -> BottomBarState.Input
|
||||
}
|
||||
|
|
@ -519,6 +523,21 @@ fun MessageMessaging(
|
|||
)
|
||||
}
|
||||
|
||||
is BottomBarState.Edit -> {
|
||||
MessageInputBar(
|
||||
isLoading = sendStatus is SendMessageState.Loading,
|
||||
replyingMessage = null, // 編輯時不顯示回覆
|
||||
editingMessage = currentState.message,
|
||||
onCancelReply = {},
|
||||
onCancelEdit = { viewModel.clearEditingMessage() },
|
||||
onSendClick = {},
|
||||
onEditSaveClick = { content ->
|
||||
viewModel.editMessage(roomId, currentState.message.id, content)
|
||||
},
|
||||
onAttachmentClick = {} // 編輯模式不給上傳附件
|
||||
)
|
||||
}
|
||||
|
||||
is BottomBarState.ActionMenu -> {
|
||||
val currentActionMessage = currentState.message
|
||||
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
|
||||
|
|
@ -532,6 +551,10 @@ fun MessageMessaging(
|
|||
viewModel.setReplyingMessage(msg)
|
||||
viewModel.clearActionMessage()
|
||||
},
|
||||
onEditClick = { msg ->
|
||||
viewModel.setEditingMessage(msg)
|
||||
viewModel.clearActionMessage()
|
||||
},
|
||||
onDeleteClick = { msg ->
|
||||
// 🌟 核心防呆:如果訊息還在發送中(SENDING),禁止點擊刪除,避免 tempId 送給後端造成 404
|
||||
if (msg.status == MessageStatus.SENDING) {
|
||||
|
|
@ -1331,6 +1354,17 @@ private fun TextMessageContent(
|
|||
text = message.content,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)
|
||||
)
|
||||
|
||||
if (message.isEdited) {
|
||||
Text(
|
||||
text = "(已編輯)",
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = 10.sp,
|
||||
color = Color.Gray.copy(alpha = 0.7f)
|
||||
),
|
||||
modifier = Modifier.align(Alignment.End).padding(top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,11 @@ class ChatViewModel @Inject constructor(
|
|||
fun setActionMessage(message: MessageUiModel?) { _actionMessage.value = message }
|
||||
fun clearActionMessage() { _actionMessage.value = null }
|
||||
|
||||
private val _editingMessage = MutableStateFlow<MessageUiModel?>(null)
|
||||
val editingMessage: StateFlow<MessageUiModel?> = _editingMessage.asStateFlow()
|
||||
fun setEditingMessage(message: MessageUiModel?) { _editingMessage.value = message }
|
||||
fun clearEditingMessage() { _editingMessage.value = null }
|
||||
|
||||
private val _replyingMessage = MutableStateFlow<MessageUiModel?>(null)
|
||||
val replyingMessage: StateFlow<MessageUiModel?> = _replyingMessage.asStateFlow()
|
||||
fun setReplyingMessage(message: MessageUiModel?) { _replyingMessage.value = message }
|
||||
|
|
@ -323,6 +328,25 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun editMessage(roomId: String, messageId: String, content: String) {
|
||||
if (content.trim().isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_sendMessageState.value = SendMessageState.Loading
|
||||
|
||||
val result = repository.editMessage(roomId, messageId, content)
|
||||
|
||||
_editingMessage.value = null
|
||||
|
||||
result.onSuccess {
|
||||
_sendMessageState.value = SendMessageState.Success
|
||||
}.onFailure { error ->
|
||||
Log.e("ChatViewModel", "Edit message failed", error)
|
||||
_sendMessageState.value = SendMessageState.Error(error.message ?: "編輯訊息失敗")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 其餘 uploadAttachment、Emoji 功能保持原樣 ---
|
||||
fun uploadAttachment(roomId: String, fileUri: Uri) {
|
||||
viewModelScope.launch {
|
||||
|
|
|
|||
Loading…
Reference in a new issue