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