feat(chat): 添加聊天界面视图模型和联系人管理功能

- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理
- 添加消息搜索、草稿保存、引用回复等功能
- 实现多媒体附件发送包括图片、视频、音频和文件
- 添加群组提及用户功能和消息撤回机制
- 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理
- 添加好友请求处理和实时消息监听
- 实现会话列表管理包含未读消息统计和实时更新
- 集成 IM SDK 的连接状态管理和事件监听
- 添加消息状态跟踪和超时处理机制
- 实现数据缓存机制优化用户体验
这个提交包含在:
XuqmGroup 2026-04-28 22:32:20 +08:00
父节点 17168dcf4e
当前提交 18f4c99b71
共有 8 个文件被更改,包括 115 次插入0 次删除

查看文件

@ -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) {

查看文件

@ -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 {

查看文件

@ -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 {

查看文件

@ -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 {

查看文件

@ -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<ImMessage>? = 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<ImMessage>? = locatePage(
maxPages = maxPages,
loadPage = { page -> fetchGroupHistory(groupId, page, pageSize) },
messageId = messageId,
pageSize = pageSize,
)
private suspend fun locatePage(
maxPages: Int,
loadPage: suspend (Int) -> List<ImMessage>,
messageId: String,
pageSize: Int,
): List<ImMessage>? {
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<ImGroup> =
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() }

查看文件

@ -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<Unit>
@PUT("api/im/messages/{messageId}")
suspend fun editMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
@Body request: EditMessageRequest,
): ApiResponse<ImMessage>
@POST("api/im/messages/{messageId}/revoke")
suspend fun revokeMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
): ApiResponse<ImMessage>
@PUT("api/im/conversations/{targetId}/draft")
suspend fun setDraft(
@Path("targetId") targetId: String,

查看文件

@ -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) {}
}

查看文件

@ -14,6 +14,10 @@ data class PageResult<T>(
val empty: Boolean = true,
)
data class EditMessageRequest(
val content: String,
)
data class ImMessage(
val id: String,
val appId: String,