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) { override fun onGroupMessage(message: ImMessage) {
handleIncomingMessage(message) handleIncomingMessage(message)
} }
override fun onRead(message: ImMessage) {
handleIncomingMessage(message)
}
override fun onRevoke(message: ImMessage) {
handleIncomingMessage(message)
}
} }
fun init(targetId: String, chatType: String) { fun init(targetId: String, chatType: String) {
@ -217,6 +225,18 @@ class ChatViewModel : ViewModel() {
_replyTargetMessage.value = null _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 sendImage(uri: Uri) = sendAttachment(uri, AttachmentKind.IMAGE)
fun sendImageBytes(fileName: String, bytes: ByteArray, width: Int? = null, height: Int? = null) { fun sendImageBytes(fileName: String, bytes: ByteArray, width: Int? = null, height: Int? = null) {

查看文件

@ -83,6 +83,8 @@ class ContactViewModel(
private val listener = object : ImEventListener { private val listener = object : ImEventListener {
override fun onMessage(message: ImMessage) = handleNotification(message) override fun onMessage(message: ImMessage) = handleNotification(message)
override fun onGroupMessage(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 { init {

查看文件

@ -49,6 +49,22 @@ class ConversationViewModel : ViewModel() {
) )
refresh() 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 { init {

查看文件

@ -203,6 +203,13 @@ class ImClient(
append(" status=").append(msg.status) 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") { if (msg.chatType.uppercase() == "GROUP") {
listeners.forEach { it.onGroupMessage(msg) } listeners.forEach { it.onGroupMessage(msg) }
} else { } else {

查看文件

@ -6,6 +6,7 @@ import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.im.api.AddMemberRequest import com.xuqm.sdk.im.api.AddMemberRequest
import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.CreateGroupRequest
import com.xuqm.sdk.im.api.ImApi 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.MuteGroupMemberRequest
import com.xuqm.sdk.im.api.SetMutedRequest import com.xuqm.sdk.im.api.SetMutedRequest
import com.xuqm.sdk.im.api.SetPinnedRequest import com.xuqm.sdk.im.api.SetPinnedRequest
@ -142,6 +143,12 @@ object ImSDK {
return sendMessage(toId, chatType, "TEXT", content, mentionedUserIds) 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( fun sendImageMessage(
toId: String, toId: String,
chatType: String, chatType: String,
@ -435,6 +442,48 @@ object ImSDK {
).data ?: emptyList() ).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> = suspend fun listGroups(): List<ImGroup> =
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() } 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.ConversationData
import com.xuqm.sdk.im.model.BlacklistEntry 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.FriendRequest
import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.GroupJoinRequest
import com.xuqm.sdk.im.model.ImMessage import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.PageResult
import com.xuqm.sdk.im.model.UserProfile import com.xuqm.sdk.im.model.UserProfile
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@ -275,6 +277,19 @@ interface ImApi {
@Query("chatType") chatType: String, @Query("chatType") chatType: String,
): ApiResponse<Unit> ): 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") @PUT("api/im/conversations/{targetId}/draft")
suspend fun setDraft( suspend fun setDraft(
@Path("targetId") targetId: String, @Path("targetId") targetId: String,

查看文件

@ -7,5 +7,7 @@ interface ImEventListener {
fun onDisconnected(reason: String?) {} fun onDisconnected(reason: String?) {}
fun onMessage(message: ImMessage) {} fun onMessage(message: ImMessage) {}
fun onGroupMessage(message: ImMessage) {} fun onGroupMessage(message: ImMessage) {}
fun onRead(message: ImMessage) {}
fun onRevoke(message: ImMessage) {}
fun onError(error: String) {} fun onError(error: String) {}
} }

查看文件

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