From 18f4c99b71791da29b002d51c7d7ebf5540de56c Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 28 Apr 2026 22:32:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=B7=BB=E5=8A=A0=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=95=8C=E9=9D=A2=E8=A7=86=E5=9B=BE=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=92=8C=E8=81=94=E7=B3=BB=E4=BA=BA=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理 - 添加消息搜索、草稿保存、引用回复等功能 - 实现多媒体附件发送包括图片、视频、音频和文件 - 添加群组提及用户功能和消息撤回机制 - 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理 - 添加好友请求处理和实时消息监听 - 实现会话列表管理包含未读消息统计和实时更新 - 集成 IM SDK 的连接状态管理和事件监听 - 添加消息状态跟踪和超时处理机制 - 实现数据缓存机制优化用户体验 --- .../xuqm/sdk/sample/ui/chat/ChatViewModel.kt | 20 ++++++++ .../sdk/sample/ui/contact/ContactScreen.kt | 2 + .../ui/conversation/ConversationViewModel.kt | 16 ++++++ .../src/main/java/com/xuqm/sdk/im/ImClient.kt | 7 +++ sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt | 49 +++++++++++++++++++ .../main/java/com/xuqm/sdk/im/api/ImApi.kt | 15 ++++++ .../xuqm/sdk/im/listener/ImEventListener.kt | 2 + .../java/com/xuqm/sdk/im/model/ImMessage.kt | 4 ++ 8 files changed, 115 insertions(+) diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt index c13c553..407fb2e 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt @@ -71,6 +71,14 @@ class ChatViewModel : ViewModel() { override fun onGroupMessage(message: ImMessage) { handleIncomingMessage(message) } + + override fun onRead(message: ImMessage) { + handleIncomingMessage(message) + } + + override fun onRevoke(message: ImMessage) { + handleIncomingMessage(message) + } } fun init(targetId: String, chatType: String) { @@ -217,6 +225,18 @@ class ChatViewModel : ViewModel() { _replyTargetMessage.value = null } + fun revokeMessage(messageId: String) { + if (!initialized) return + viewModelScope.launch { + runCatching { ImSDK.revokeMessage(messageId) } + .onSuccess { revoked -> handleIncomingMessage(revoked) } + .onFailure { + _events.tryEmit("消息撤回失败,请检查网络后重试") + Log.w(TAG, "revokeMessage failed messageId=$messageId", it) + } + } + } + fun sendImage(uri: Uri) = sendAttachment(uri, AttachmentKind.IMAGE) fun sendImageBytes(fileName: String, bytes: ByteArray, width: Int? = null, height: Int? = null) { diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/contact/ContactScreen.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/contact/ContactScreen.kt index b1a9bb2..add457d 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/contact/ContactScreen.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/contact/ContactScreen.kt @@ -83,6 +83,8 @@ class ContactViewModel( private val listener = object : ImEventListener { override fun onMessage(message: ImMessage) = handleNotification(message) override fun onGroupMessage(message: ImMessage) = handleNotification(message) + override fun onRead(message: ImMessage) = handleNotification(message) + override fun onRevoke(message: ImMessage) = handleNotification(message) } init { diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/conversation/ConversationViewModel.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/conversation/ConversationViewModel.kt index 0df402f..09c24ee 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/conversation/ConversationViewModel.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/conversation/ConversationViewModel.kt @@ -49,6 +49,22 @@ class ConversationViewModel : ViewModel() { ) refresh() } + + override fun onRead(message: ImMessage) { + Log.d( + TAG, + "incoming read refresh request id=${message.id} from=${message.fromId} to=${message.toId} msgType=${message.msgType}", + ) + refresh() + } + + override fun onRevoke(message: ImMessage) { + Log.d( + TAG, + "incoming revoke refresh request id=${message.id} from=${message.fromId} to=${message.toId} msgType=${message.msgType}", + ) + refresh() + } } init { diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt index 7e4de82..991a861 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt @@ -203,6 +203,13 @@ class ImClient( append(" status=").append(msg.status) }, ) + if (msg.status.uppercase() == "READ") { + listeners.forEach { it.onRead(msg) } + } + if (msg.status.uppercase() == "REVOKED" || msg.msgType.uppercase() == "REVOKED") { + listeners.forEach { it.onRevoke(msg) } + return + } if (msg.chatType.uppercase() == "GROUP") { listeners.forEach { it.onGroupMessage(msg) } } else { diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt index bdd6007..1b2288f 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt @@ -6,6 +6,7 @@ import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.im.api.AddMemberRequest import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.ImApi +import com.xuqm.sdk.im.model.EditMessageRequest import com.xuqm.sdk.im.api.MuteGroupMemberRequest import com.xuqm.sdk.im.api.SetMutedRequest import com.xuqm.sdk.im.api.SetPinnedRequest @@ -142,6 +143,12 @@ object ImSDK { return sendMessage(toId, chatType, "TEXT", content, mentionedUserIds) } + suspend fun editMessage(messageId: String, content: String): ImMessage = + withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appId, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") } + + suspend fun revokeMessage(messageId: String): ImMessage = + withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appId).data ?: throw IllegalStateException("revoke message failed") } + fun sendImageMessage( toId: String, chatType: String, @@ -435,6 +442,48 @@ object ImSDK { ).data ?: emptyList() } + suspend fun locateHistoryPage( + toId: String, + messageId: String, + pageSize: Int = 20, + maxPages: Int = 20, + ): List? = locatePage( + maxPages = maxPages, + loadPage = { page -> fetchHistory(toId, page, pageSize) }, + messageId = messageId, + pageSize = pageSize, + ) + + suspend fun locateGroupHistoryPage( + groupId: String, + messageId: String, + pageSize: Int = 20, + maxPages: Int = 20, + ): List? = locatePage( + maxPages = maxPages, + loadPage = { page -> fetchGroupHistory(groupId, page, pageSize) }, + messageId = messageId, + pageSize = pageSize, + ) + + private suspend fun locatePage( + maxPages: Int, + loadPage: suspend (Int) -> List, + messageId: String, + pageSize: Int, + ): List? { + repeat(maxPages.coerceAtLeast(1)) { page -> + val messages = loadPage(page) + if (messages.any { it.id == messageId }) { + return messages + } + if (messages.size < pageSize) { + return null + } + } + return null + } + suspend fun listGroups(): List = withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt index f158b15..f6ea387 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt @@ -2,10 +2,12 @@ package com.xuqm.sdk.im.api import com.xuqm.sdk.im.model.ConversationData import com.xuqm.sdk.im.model.BlacklistEntry +import com.xuqm.sdk.im.model.EditMessageRequest import com.xuqm.sdk.im.model.FriendRequest import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.ImMessage +import com.xuqm.sdk.im.model.PageResult import com.xuqm.sdk.im.model.UserProfile import retrofit2.http.Body import retrofit2.http.DELETE @@ -275,6 +277,19 @@ interface ImApi { @Query("chatType") chatType: String, ): ApiResponse + @PUT("api/im/messages/{messageId}") + suspend fun editMessage( + @Path("messageId") messageId: String, + @Query("appId") appId: String, + @Body request: EditMessageRequest, + ): ApiResponse + + @POST("api/im/messages/{messageId}/revoke") + suspend fun revokeMessage( + @Path("messageId") messageId: String, + @Query("appId") appId: String, + ): ApiResponse + @PUT("api/im/conversations/{targetId}/draft") suspend fun setDraft( @Path("targetId") targetId: String, diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/listener/ImEventListener.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/listener/ImEventListener.kt index 71941e7..1d61d17 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/listener/ImEventListener.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/listener/ImEventListener.kt @@ -7,5 +7,7 @@ interface ImEventListener { fun onDisconnected(reason: String?) {} fun onMessage(message: ImMessage) {} fun onGroupMessage(message: ImMessage) {} + fun onRead(message: ImMessage) {} + fun onRevoke(message: ImMessage) {} fun onError(error: String) {} } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt index 0a1aea3..8d225bd 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt @@ -14,6 +14,10 @@ data class PageResult( val empty: Boolean = true, ) +data class EditMessageRequest( + val content: String, +) + data class ImMessage( val id: String, val appId: String,