feat(chat): 添加聊天界面视图模型和联系人管理功能
- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理 - 添加消息搜索、草稿保存、引用回复等功能 - 实现多媒体附件发送包括图片、视频、音频和文件 - 添加群组提及用户功能和消息撤回机制 - 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理 - 添加好友请求处理和实时消息监听 - 实现会话列表管理包含未读消息统计和实时更新 - 集成 IM SDK 的连接状态管理和事件监听 - 添加消息状态跟踪和超时处理机制 - 实现数据缓存机制优化用户体验
这个提交包含在:
父节点
17168dcf4e
当前提交
18f4c99b71
@ -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,
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户