feat(im): 添加即时消息SDK核心功能实现
- 实现了聊天消息发送功能,支持文本、图片、视频、音频、文件等多种消息类型 - 集成了文件上传下载功能,支持多媒体文件的传输和管理 - 添加了群组管理功能,包括创建群组、成员管理、权限控制等操作 - 实现了好友系统,支持好友添加、删除、分组等功能 - 集成了黑名单管理,提供用户屏蔽和解除屏蔽功能 - 添加了会话管理功能,支持对话列表、未读消息统计等 - 实现了历史消息查询和搜索功能 - 添加了实时连接状态管理和自动重连机制
这个提交包含在:
父节点
d9c9e4f858
当前提交
d0b263411d
@ -6,16 +6,14 @@ import android.graphics.BitmapFactory
|
|||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import com.xuqm.sdk.file.FileSDK
|
|
||||||
import com.xuqm.sdk.file.FileUploadResult
|
|
||||||
import com.xuqm.sdk.im.ImSDK
|
import com.xuqm.sdk.im.ImSDK
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
data class PreparedAttachment(
|
data class PreparedAttachment(
|
||||||
val upload: FileUploadResult,
|
|
||||||
val message: ImMessage,
|
val message: ImMessage,
|
||||||
val width: Int? = null,
|
val width: Int? = null,
|
||||||
val height: Int? = null,
|
val height: Int? = null,
|
||||||
@ -29,8 +27,20 @@ class AttachmentRepository(private val context: Context) {
|
|||||||
chatType: String,
|
chatType: String,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> =
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
sendMedia(targetId, chatType, uri, MediaKind.IMAGE, onProgress)
|
runCatching {
|
||||||
|
val file = uriToFile(uri)
|
||||||
|
val meta = resolveMeta(uri)
|
||||||
|
val sent = ImSDK.sendImageMessage(
|
||||||
|
toId = targetId,
|
||||||
|
chatType = chatType,
|
||||||
|
file = file,
|
||||||
|
width = meta.width,
|
||||||
|
height = meta.height,
|
||||||
|
)
|
||||||
|
PreparedAttachment(message = sent, width = meta.width, height = meta.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendImageBytes(
|
suspend fun sendImageBytes(
|
||||||
targetId: String,
|
targetId: String,
|
||||||
@ -42,21 +52,15 @@ class AttachmentRepository(private val context: Context) {
|
|||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val upload = FileSDK.uploadBytes(
|
val file = File(context.cacheDir, fileName).apply { writeBytes(bytes) }
|
||||||
fileName = fileName,
|
val sent = ImSDK.sendImageMessage(
|
||||||
mimeType = "image/jpeg",
|
|
||||||
bytes = bytes,
|
|
||||||
onProgress = onProgress,
|
|
||||||
)
|
|
||||||
ImSDK.sendImageMessage(
|
|
||||||
toId = targetId,
|
toId = targetId,
|
||||||
chatType = chatType,
|
chatType = chatType,
|
||||||
file = upload,
|
file = file,
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
).let { sent ->
|
)
|
||||||
PreparedAttachment(upload = upload, message = sent, width = width, height = height)
|
PreparedAttachment(message = sent, width = width, height = height)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,24 +69,57 @@ class AttachmentRepository(private val context: Context) {
|
|||||||
chatType: String,
|
chatType: String,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> =
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
sendMedia(targetId, chatType, uri, MediaKind.VIDEO, onProgress)
|
runCatching {
|
||||||
|
val file = uriToFile(uri)
|
||||||
|
val meta = resolveMeta(uri)
|
||||||
|
val sent = ImSDK.sendVideoMessage(
|
||||||
|
toId = targetId,
|
||||||
|
chatType = chatType,
|
||||||
|
file = file,
|
||||||
|
width = meta.width,
|
||||||
|
height = meta.height,
|
||||||
|
durationMs = meta.durationMs,
|
||||||
|
)
|
||||||
|
PreparedAttachment(message = sent, width = meta.width, height = meta.height, durationMs = meta.durationMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendFile(
|
suspend fun sendFile(
|
||||||
targetId: String,
|
targetId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> =
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
sendMedia(targetId, chatType, uri, MediaKind.FILE, onProgress)
|
runCatching {
|
||||||
|
val file = uriToFile(uri)
|
||||||
|
val sent = ImSDK.sendFileMessage(
|
||||||
|
toId = targetId,
|
||||||
|
chatType = chatType,
|
||||||
|
file = file,
|
||||||
|
)
|
||||||
|
PreparedAttachment(message = sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendAudio(
|
suspend fun sendAudio(
|
||||||
targetId: String,
|
targetId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> =
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
sendMedia(targetId, chatType, uri, MediaKind.AUDIO, onProgress)
|
runCatching {
|
||||||
|
val file = uriToFile(uri)
|
||||||
|
val meta = resolveMeta(uri)
|
||||||
|
val sent = ImSDK.sendAudioMessage(
|
||||||
|
toId = targetId,
|
||||||
|
chatType = chatType,
|
||||||
|
file = file,
|
||||||
|
durationMs = meta.durationMs,
|
||||||
|
)
|
||||||
|
PreparedAttachment(message = sent, durationMs = meta.durationMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendAudioBytes(
|
suspend fun sendAudioBytes(
|
||||||
targetId: String,
|
targetId: String,
|
||||||
@ -93,82 +130,31 @@ class AttachmentRepository(private val context: Context) {
|
|||||||
onProgress: (Int) -> Unit = {},
|
onProgress: (Int) -> Unit = {},
|
||||||
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val upload = FileSDK.uploadBytes(
|
val file = File(context.cacheDir, fileName).apply { writeBytes(bytes) }
|
||||||
fileName = fileName,
|
val sent = ImSDK.sendAudioMessage(
|
||||||
mimeType = "audio/mp4",
|
|
||||||
bytes = bytes,
|
|
||||||
onProgress = onProgress,
|
|
||||||
)
|
|
||||||
ImSDK.sendAudioMessage(
|
|
||||||
toId = targetId,
|
toId = targetId,
|
||||||
chatType = chatType,
|
chatType = chatType,
|
||||||
file = upload,
|
file = file,
|
||||||
durationMs = durationMs,
|
durationMs = durationMs,
|
||||||
).let { sent ->
|
)
|
||||||
PreparedAttachment(upload = upload, message = sent, durationMs = durationMs)
|
PreparedAttachment(message = sent, durationMs = durationMs)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private fun uriToFile(uri: Uri): File {
|
||||||
targetId: String,
|
val resolver = context.contentResolver
|
||||||
chatType: String,
|
val displayName = resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
||||||
uri: Uri,
|
?.use { cursor ->
|
||||||
kind: MediaKind,
|
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
onProgress: (Int) -> Unit,
|
if (index >= 0 && cursor.moveToFirst()) cursor.getString(index) else null
|
||||||
): Result<PreparedAttachment> = withContext(Dispatchers.IO) {
|
} ?: uri.lastPathSegment?.substringAfterLast('/') ?: "attachment"
|
||||||
runCatching {
|
val tempFile = File(context.cacheDir, displayName)
|
||||||
val meta = resolveMeta(uri)
|
resolver.openInputStream(uri)?.use { input ->
|
||||||
val thumbnailBytes = when (kind) {
|
tempFile.outputStream().use { output ->
|
||||||
MediaKind.IMAGE -> null
|
input.copyTo(output)
|
||||||
MediaKind.VIDEO -> extractVideoThumbnail(uri)
|
|
||||||
MediaKind.AUDIO -> null
|
|
||||||
MediaKind.FILE -> null
|
|
||||||
}
|
}
|
||||||
val upload = FileSDK.upload(
|
|
||||||
context = context,
|
|
||||||
uri = uri,
|
|
||||||
displayName = meta.displayName,
|
|
||||||
mimeType = meta.mimeType,
|
|
||||||
thumbnailBytes = thumbnailBytes,
|
|
||||||
onProgress = onProgress,
|
|
||||||
)
|
|
||||||
val sent = when (kind) {
|
|
||||||
MediaKind.IMAGE -> ImSDK.sendImageMessage(
|
|
||||||
toId = targetId,
|
|
||||||
chatType = chatType,
|
|
||||||
file = upload,
|
|
||||||
width = meta.width,
|
|
||||||
height = meta.height,
|
|
||||||
)
|
|
||||||
MediaKind.VIDEO -> ImSDK.sendVideoMessage(
|
|
||||||
toId = targetId,
|
|
||||||
chatType = chatType,
|
|
||||||
file = upload,
|
|
||||||
width = meta.width,
|
|
||||||
height = meta.height,
|
|
||||||
durationMs = meta.durationMs,
|
|
||||||
)
|
|
||||||
MediaKind.AUDIO -> ImSDK.sendAudioMessage(
|
|
||||||
toId = targetId,
|
|
||||||
chatType = chatType,
|
|
||||||
file = upload,
|
|
||||||
durationMs = meta.durationMs,
|
|
||||||
)
|
|
||||||
MediaKind.FILE -> ImSDK.sendFileMessage(
|
|
||||||
toId = targetId,
|
|
||||||
chatType = chatType,
|
|
||||||
file = upload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PreparedAttachment(
|
|
||||||
upload = upload,
|
|
||||||
message = sent,
|
|
||||||
width = meta.width,
|
|
||||||
height = meta.height,
|
|
||||||
durationMs = meta.durationMs,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return tempFile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveMeta(uri: Uri): AttachmentMeta {
|
private fun resolveMeta(uri: Uri): AttachmentMeta {
|
||||||
|
|||||||
@ -105,6 +105,16 @@ object FileSDK {
|
|||||||
targetFile
|
targetFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun upload(
|
||||||
|
file: File,
|
||||||
|
thumbnailBytes: ByteArray? = null,
|
||||||
|
onProgress: (Int) -> Unit = {},
|
||||||
|
): FileUploadResult {
|
||||||
|
val mimeType = java.net.URLConnection.guessContentTypeFromName(file.name)
|
||||||
|
val bytes = file.readBytes()
|
||||||
|
return uploadBytes(file.name, mimeType, bytes, thumbnailBytes, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun downloadToAppFiles(
|
suspend fun downloadToAppFiles(
|
||||||
context: Context,
|
context: Context,
|
||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import com.xuqm.sdk.im.model.PageResult
|
|||||||
import com.xuqm.sdk.im.model.UserProfile
|
import com.xuqm.sdk.im.model.UserProfile
|
||||||
import com.xuqm.sdk.im.model.BlacklistEntry
|
import com.xuqm.sdk.im.model.BlacklistEntry
|
||||||
import com.xuqm.sdk.im.model.FriendRequest
|
import com.xuqm.sdk.im.model.FriendRequest
|
||||||
|
import com.xuqm.sdk.file.FileSDK
|
||||||
import com.xuqm.sdk.file.FileUploadResult
|
import com.xuqm.sdk.file.FileUploadResult
|
||||||
import com.xuqm.sdk.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -44,6 +45,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -59,6 +61,9 @@ object ImSDK {
|
|||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val activeGroupSubscriptions = linkedSetOf<String>()
|
private val activeGroupSubscriptions = linkedSetOf<String>()
|
||||||
private val listeners = CopyOnWriteArraySet<ImEventListener>()
|
private val listeners = CopyOnWriteArraySet<ImEventListener>()
|
||||||
|
private val conversationListeners = CopyOnWriteArraySet<ConversationListener>()
|
||||||
|
private val _conversations = mutableListOf<ConversationData>()
|
||||||
|
private val conversationsLock = Any()
|
||||||
private var reconnectJob: Job? = null
|
private var reconnectJob: Job? = null
|
||||||
private var reconnectAttempts = 0
|
private var reconnectAttempts = 0
|
||||||
@Volatile private var reconnectEnabled = false
|
@Volatile private var reconnectEnabled = false
|
||||||
@ -89,6 +94,10 @@ object ImSDK {
|
|||||||
var currentUserId: String = ""
|
var currentUserId: String = ""
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
interface ConversationListener {
|
||||||
|
fun onConversationsChanged(conversations: List<ConversationData>)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
XuqmSDK.currentLoginSession?.let { onSdkLogin(it) }
|
XuqmSDK.currentLoginSession?.let { onSdkLogin(it) }
|
||||||
}
|
}
|
||||||
@ -141,7 +150,18 @@ object ImSDK {
|
|||||||
suspend fun revokeMessage(messageId: String): ImMessage =
|
suspend fun revokeMessage(messageId: String): ImMessage =
|
||||||
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appKey).data ?: throw IllegalStateException("revoke message failed") }
|
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appKey).data ?: throw IllegalStateException("revoke message failed") }
|
||||||
|
|
||||||
fun sendImageMessage(
|
suspend fun sendImageMessage(
|
||||||
|
toId: String,
|
||||||
|
chatType: String,
|
||||||
|
file: File,
|
||||||
|
width: Int? = null,
|
||||||
|
height: Int? = null,
|
||||||
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
|
val result = FileSDK.upload(file)
|
||||||
|
sendImageMessageWithUploadResult(toId, chatType, result, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendImageMessageWithUploadResult(
|
||||||
toId: String,
|
toId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
file: FileUploadResult,
|
file: FileUploadResult,
|
||||||
@ -165,7 +185,19 @@ object ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendVideoMessage(
|
suspend fun sendVideoMessage(
|
||||||
|
toId: String,
|
||||||
|
chatType: String,
|
||||||
|
file: File,
|
||||||
|
width: Int? = null,
|
||||||
|
height: Int? = null,
|
||||||
|
durationMs: Long? = null,
|
||||||
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
|
val result = FileSDK.upload(file)
|
||||||
|
sendVideoMessageWithUploadResult(toId, chatType, result, width, height, durationMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendVideoMessageWithUploadResult(
|
||||||
toId: String,
|
toId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
file: FileUploadResult,
|
file: FileUploadResult,
|
||||||
@ -191,7 +223,16 @@ object ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFileMessage(
|
suspend fun sendFileMessage(
|
||||||
|
toId: String,
|
||||||
|
chatType: String,
|
||||||
|
file: File,
|
||||||
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
|
val result = FileSDK.upload(file)
|
||||||
|
sendFileMessageWithUploadResult(toId, chatType, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendFileMessageWithUploadResult(
|
||||||
toId: String,
|
toId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
file: FileUploadResult,
|
file: FileUploadResult,
|
||||||
@ -210,7 +251,17 @@ object ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendAudioMessage(
|
suspend fun sendAudioMessage(
|
||||||
|
toId: String,
|
||||||
|
chatType: String,
|
||||||
|
file: File,
|
||||||
|
durationMs: Long? = null,
|
||||||
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
|
val result = FileSDK.upload(file)
|
||||||
|
sendAudioMessageWithUploadResult(toId, chatType, result, durationMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendAudioMessageWithUploadResult(
|
||||||
toId: String,
|
toId: String,
|
||||||
chatType: String,
|
chatType: String,
|
||||||
file: FileUploadResult,
|
file: FileUploadResult,
|
||||||
@ -625,6 +676,13 @@ object ImSDK {
|
|||||||
|
|
||||||
suspend fun listConversations(): List<ConversationData> =
|
suspend fun listConversations(): List<ConversationData> =
|
||||||
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appKey).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
.also { result ->
|
||||||
|
synchronized(conversationsLock) {
|
||||||
|
_conversations.clear()
|
||||||
|
_conversations.addAll(result)
|
||||||
|
}
|
||||||
|
notifyConversationListeners()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getTotalUnreadCount(): Int =
|
suspend fun getTotalUnreadCount(): Int =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@ -720,6 +778,16 @@ object ImSDK {
|
|||||||
client?.removeListener(listener)
|
client?.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addConversationListener(listener: ConversationListener) {
|
||||||
|
Log.d(TAG, "addConversationListener listener=${listener.javaClass.name}")
|
||||||
|
conversationListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeConversationListener(listener: ConversationListener) {
|
||||||
|
Log.d(TAG, "removeConversationListener listener=${listener.javaClass.name}")
|
||||||
|
conversationListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
disconnectInternal(clearTokenStore = true)
|
disconnectInternal(clearTokenStore = true)
|
||||||
}
|
}
|
||||||
@ -749,6 +817,7 @@ object ImSDK {
|
|||||||
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
|
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
|
||||||
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appKey)
|
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appKey)
|
||||||
client?.addListener(connectionListener)
|
client?.addListener(connectionListener)
|
||||||
|
client?.addListener(conversationEventListener)
|
||||||
listeners.forEach { client?.addListener(it) }
|
listeners.forEach { client?.addListener(it) }
|
||||||
reconnectEnabled = true
|
reconnectEnabled = true
|
||||||
client?.connect()
|
client?.connect()
|
||||||
@ -832,6 +901,88 @@ object ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val conversationEventListener = object : ImEventListener {
|
||||||
|
override fun onMessage(message: ImMessage) { onMessageReceived(message) }
|
||||||
|
override fun onGroupMessage(message: ImMessage) { onMessageReceived(message) }
|
||||||
|
override fun onRead(message: ImMessage) { onReadReceived(message) }
|
||||||
|
override fun onRevoke(message: ImMessage) { onRevokeReceived(message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConversationTargetId(message: ImMessage): String {
|
||||||
|
return if (message.chatType.uppercase() == "GROUP") {
|
||||||
|
message.toId
|
||||||
|
} else {
|
||||||
|
if (message.fromId == currentUserId) message.toId else message.fromId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMessageReceived(message: ImMessage) {
|
||||||
|
val targetId = getConversationTargetId(message)
|
||||||
|
val chatType = message.chatType.uppercase()
|
||||||
|
synchronized(conversationsLock) {
|
||||||
|
val index = _conversations.indexOfFirst { it.targetId == targetId && it.chatType == chatType }
|
||||||
|
val existing = _conversations.getOrNull(index)
|
||||||
|
val updated = if (existing != null) {
|
||||||
|
existing.copy(
|
||||||
|
lastMsgContent = message.content,
|
||||||
|
lastMsgType = message.msgType,
|
||||||
|
lastMsgTime = message.createdAt,
|
||||||
|
unreadCount = if (message.fromId != currentUserId) existing.unreadCount + 1 else existing.unreadCount
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConversationData(
|
||||||
|
targetId = targetId,
|
||||||
|
chatType = chatType,
|
||||||
|
lastMsgContent = message.content,
|
||||||
|
lastMsgType = message.msgType,
|
||||||
|
lastMsgTime = message.createdAt,
|
||||||
|
unreadCount = if (message.fromId != currentUserId) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
_conversations[index] = updated
|
||||||
|
} else {
|
||||||
|
_conversations.add(updated)
|
||||||
|
}
|
||||||
|
_conversations.sortByDescending { it.lastMsgTime }
|
||||||
|
}
|
||||||
|
notifyConversationListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReadReceived(message: ImMessage) {
|
||||||
|
val targetId = getConversationTargetId(message)
|
||||||
|
val chatType = message.chatType.uppercase()
|
||||||
|
synchronized(conversationsLock) {
|
||||||
|
val index = _conversations.indexOfFirst { it.targetId == targetId && it.chatType == chatType }
|
||||||
|
if (index < 0) return
|
||||||
|
val existing = _conversations[index]
|
||||||
|
_conversations[index] = existing.copy(unreadCount = 0)
|
||||||
|
}
|
||||||
|
notifyConversationListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRevokeReceived(message: ImMessage) {
|
||||||
|
val targetId = getConversationTargetId(message)
|
||||||
|
val chatType = message.chatType.uppercase()
|
||||||
|
synchronized(conversationsLock) {
|
||||||
|
val index = _conversations.indexOfFirst { it.targetId == targetId && it.chatType == chatType }
|
||||||
|
if (index < 0) return
|
||||||
|
val existing = _conversations[index]
|
||||||
|
val updated = if (existing.lastMsgTime == message.createdAt) {
|
||||||
|
existing.copy(lastMsgContent = "消息已撤回", lastMsgType = "REVOKED")
|
||||||
|
} else {
|
||||||
|
existing
|
||||||
|
}
|
||||||
|
_conversations[index] = updated
|
||||||
|
}
|
||||||
|
notifyConversationListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyConversationListeners() {
|
||||||
|
val snapshot = synchronized(conversationsLock) { _conversations.toList() }
|
||||||
|
conversationListeners.forEach { it.onConversationsChanged(snapshot) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun scheduleReconnect(reason: String?) {
|
private fun scheduleReconnect(reason: String?) {
|
||||||
if (!reconnectEnabled || currentToken.isBlank()) return
|
if (!reconnectEnabled || currentToken.isBlank()) return
|
||||||
if (reconnectJob?.isActive == true) return
|
if (reconnectJob?.isActive == true) return
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户