feat(chat): 添加聊天界面和本地缓存功能

- 新增 ChatScreen 和 ChatViewModel 实现聊天界面
- 新增 ConversationScreen 和 ConversationViewModel 实现会话列表
- 添加 LocalImCache 类实现本地消息和会话缓存
- 集成 IM SDK 的消息发送、接收和历史记录功能
- 实现消息搜索和分页加载功能
- 添加会话置顶、免打扰等管理功能
- 在 AppDependencies 中注册本地缓存依赖
这个提交包含在:
XuqmGroup 2026-04-27 19:45:45 +08:00
父节点 64fefd1bbb
当前提交 efe2a32a00
共有 6 个文件被更改,包括 356 次插入39 次删除

查看文件

@ -0,0 +1,160 @@
package com.xuqm.sdk.sample.data.local
import android.content.Context
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImMessage
import org.json.JSONArray
import org.json.JSONObject
class LocalImCache(context: Context) {
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fun loadConversations(): List<ConversationData> = readConversationList(KEY_CONVERSATIONS)
fun saveConversations(conversations: List<ConversationData>) {
prefs.edit().putString(KEY_CONVERSATIONS, serializeConversationList(conversations)).apply()
}
fun loadHistory(targetId: String, chatType: String): List<ImMessage> =
readMessageList(historyKey(targetId, chatType))
fun saveHistory(targetId: String, chatType: String, messages: List<ImMessage>) {
prefs.edit().putString(historyKey(targetId, chatType), serializeMessageList(messages)).apply()
}
fun getHistoryPage(targetId: String, chatType: String, page: Int, size: Int): List<ImMessage> {
val history = loadHistory(targetId, chatType).sortedByDescending { it.createdAt }
val fromIndex = page * size
if (fromIndex >= history.size) return emptyList()
return history.drop(fromIndex).take(size)
}
fun searchHistory(
targetId: String,
chatType: String,
keyword: String,
): List<ImMessage> {
val normalized = keyword.trim()
if (normalized.isBlank()) return emptyList()
return loadHistory(targetId, chatType)
.filter { it.matches(normalized) }
.sortedByDescending { it.createdAt }
}
fun mergeHistory(targetId: String, chatType: String, messages: List<ImMessage>) {
val merged = (loadHistory(targetId, chatType) + messages)
.distinctBy { it.id }
.sortedByDescending { it.createdAt }
saveHistory(targetId, chatType, merged)
}
private fun readConversationList(key: String): List<ConversationData> {
val raw = prefs.getString(key, null) ?: return emptyList()
return runCatching {
val array = JSONArray(raw)
buildList {
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
add(
ConversationData(
targetId = obj.optString("targetId"),
chatType = obj.optString("chatType"),
lastMsgContent = obj.optString("lastMsgContent").takeIf { it.isNotBlank() },
lastMsgType = obj.optString("lastMsgType").takeIf { it.isNotBlank() },
lastMsgTime = obj.optLong("lastMsgTime"),
unreadCount = obj.optInt("unreadCount"),
isMuted = obj.optBoolean("isMuted"),
isPinned = obj.optBoolean("isPinned"),
)
)
}
}
}.getOrDefault(emptyList())
}
private fun readMessageList(key: String): List<ImMessage> {
val raw = prefs.getString(key, null) ?: return emptyList()
return runCatching {
val array = JSONArray(raw)
buildList {
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
add(
ImMessage(
id = obj.optString("id"),
appId = obj.optString("appId"),
fromId = obj.optString("fromId"),
toId = obj.optString("toId"),
chatType = obj.optString("chatType"),
msgType = obj.optString("msgType"),
content = obj.optString("content"),
status = obj.optString("status"),
mentionedUserIds = obj.optString("mentionedUserIds").takeIf { it.isNotBlank() },
createdAt = obj.optLong("createdAt"),
)
)
}
}
}.getOrDefault(emptyList())
}
private fun serializeConversationList(conversations: List<ConversationData>): String {
val array = JSONArray()
conversations.forEach { conversation ->
array.put(
JSONObject().apply {
put("targetId", conversation.targetId)
put("chatType", conversation.chatType)
put("lastMsgContent", conversation.lastMsgContent ?: "")
put("lastMsgType", conversation.lastMsgType ?: "")
put("lastMsgTime", conversation.lastMsgTime)
put("unreadCount", conversation.unreadCount)
put("isMuted", conversation.isMuted)
put("isPinned", conversation.isPinned)
}
)
}
return array.toString()
}
private fun serializeMessageList(messages: List<ImMessage>): String {
val array = JSONArray()
messages.forEach { message ->
array.put(
JSONObject().apply {
put("id", message.id)
put("appId", message.appId)
put("fromId", message.fromId)
put("toId", message.toId)
put("chatType", message.chatType)
put("msgType", message.msgType)
put("content", message.content)
put("status", message.status)
put("mentionedUserIds", message.mentionedUserIds ?: "")
put("createdAt", message.createdAt)
}
)
}
return array.toString()
}
private fun ImMessage.matches(keyword: String): Boolean {
val plainText = when (msgType.uppercase()) {
"TEXT" -> runCatching { JSONObject(content).optString("text") }.getOrDefault(content)
"NOTIFY" -> runCatching { JSONObject(content).optString("content") }.getOrDefault(content)
else -> content
}
return plainText.contains(keyword, ignoreCase = true) ||
fromId.contains(keyword, ignoreCase = true) ||
toId.contains(keyword, ignoreCase = true) ||
msgType.contains(keyword, ignoreCase = true)
}
private fun historyKey(targetId: String, chatType: String) = "history_${chatType}_$targetId"
private companion object {
const val PREFS_NAME = "xuqm_demo_im_cache"
const val KEY_CONVERSATIONS = "conversations"
}
}

查看文件

@ -2,7 +2,9 @@ package com.xuqm.sdk.sample.di
import android.content.Context import android.content.Context
import com.xuqm.sdk.sample.data.repo.AuthRepository import com.xuqm.sdk.sample.data.repo.AuthRepository
import com.xuqm.sdk.sample.data.local.LocalContactCache
import com.xuqm.sdk.sample.data.repo.EnvironmentRepository import com.xuqm.sdk.sample.data.repo.EnvironmentRepository
import com.xuqm.sdk.sample.data.local.LocalImCache
object AppDependencies { object AppDependencies {
@ -10,10 +12,16 @@ object AppDependencies {
private set private set
lateinit var environmentRepository: EnvironmentRepository lateinit var environmentRepository: EnvironmentRepository
private set private set
lateinit var localImCache: LocalImCache
private set
lateinit var localContactCache: LocalContactCache
private set
fun init(context: Context) { fun init(context: Context) {
environmentRepository = EnvironmentRepository(context) environmentRepository = EnvironmentRepository(context)
environmentRepository.current() environmentRepository.current()
localImCache = LocalImCache(context)
localContactCache = LocalContactCache(context)
authRepository = AuthRepository(context) authRepository = AuthRepository(context)
} }
} }

查看文件

@ -2,7 +2,6 @@ package com.xuqm.sdk.sample.ui.chat
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -18,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -41,10 +41,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.ImSDK import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.ui.InitialAvatar import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner
import com.xuqm.sdk.ui.InitialAvatar
import com.xuqm.sdk.ui.SearchBarField
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -58,18 +59,24 @@ fun ChatScreen(
viewModel: ChatViewModel = viewModel(), viewModel: ChatViewModel = viewModel(),
) { ) {
val messages by viewModel.messages.collectAsStateWithLifecycle() val messages by viewModel.messages.collectAsStateWithLifecycle()
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle() val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle() val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle() val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle() val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var input by remember { mutableStateOf("") } var input by remember { mutableStateOf("") }
var showSearchBar by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) } LaunchedEffect(Unit) { viewModel.init(targetId, chatType) }
LaunchedEffect(scrollSignal) { LaunchedEffect(scrollSignal) {
if (messages.isNotEmpty()) listState.animateScrollToItem(0) if (messages.isNotEmpty() && searchQuery.isBlank()) {
listState.animateScrollToItem(0)
} }
LaunchedEffect(listState, hasMoreHistory, isLoadingMore, messages.size) { }
LaunchedEffect(listState, hasMoreHistory, isLoadingMore, messages.size, searchQuery) {
if (searchQuery.isNotBlank()) return@LaunchedEffect
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 } snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 }
.distinctUntilChanged() .distinctUntilChanged()
.collect { lastVisibleIndex -> .collect { lastVisibleIndex ->
@ -94,6 +101,16 @@ fun ChatScreen(
} }
}, },
actions = { actions = {
IconButton(
onClick = {
showSearchBar = !showSearchBar
if (!showSearchBar) {
viewModel.clearSearch()
}
},
) {
Icon(Icons.Default.Search, contentDescription = "搜索")
}
if (chatType == "GROUP" && onGroupSettings != null) { if (chatType == "GROUP" && onGroupSettings != null) {
IconButton(onClick = onGroupSettings) { IconButton(onClick = onGroupSettings) {
Icon(Icons.Default.Settings, contentDescription = null) Icon(Icons.Default.Settings, contentDescription = null)
@ -129,12 +146,52 @@ fun ChatScreen(
} }
}, },
) { padding -> ) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
if (showSearchBar || searchQuery.isNotBlank()) {
SearchBarField(
value = searchQuery,
onValueChange = viewModel::searchCachedMessages,
placeholder = "搜索当前会话本地消息",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
)
}
if (searchQuery.isNotBlank()) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
items(searchResults, key = { it.id }) { msg ->
MessageBubble(
message = msg,
isOwn = msg.fromId == viewModel.currentUserId,
)
}
if (searchResults.isEmpty()) {
item {
Text(
"没有找到匹配的本地消息",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
}
} else {
LazyColumn( LazyColumn(
state = listState, state = listState,
reverseLayout = true, reverseLayout = true,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.padding(padding), .weight(1f),
) { ) {
items(messages, key = { it.id }) { msg -> items(messages, key = { it.id }) { msg ->
MessageBubble( MessageBubble(
@ -157,6 +214,8 @@ fun ChatScreen(
} }
} }
} }
}
}
@Composable @Composable
private fun MessageBubble(message: ImMessage, isOwn: Boolean) { private fun MessageBubble(message: ImMessage, isOwn: Boolean) {

查看文件

@ -5,12 +5,14 @@ import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ImMessage import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ChatViewModel : ViewModel() { class ChatViewModel : ViewModel() {
private val cache = AppDependencies.localImCache
private val _messages = MutableStateFlow<List<ImMessage>>(emptyList()) private val _messages = MutableStateFlow<List<ImMessage>>(emptyList())
val messages: StateFlow<List<ImMessage>> = _messages val messages: StateFlow<List<ImMessage>> = _messages
@ -23,6 +25,12 @@ class ChatViewModel : ViewModel() {
private val _scrollToBottomSignal = MutableStateFlow(0L) private val _scrollToBottomSignal = MutableStateFlow(0L)
val scrollToBottomSignal: StateFlow<Long> = _scrollToBottomSignal val scrollToBottomSignal: StateFlow<Long> = _scrollToBottomSignal
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery
private val _searchResults = MutableStateFlow<List<ImMessage>>(emptyList())
val searchResults: StateFlow<List<ImMessage>> = _searchResults
val currentUserId: String get() = ImSDK.currentUserId val currentUserId: String get() = ImSDK.currentUserId
private lateinit var targetId: String private lateinit var targetId: String
@ -34,6 +42,10 @@ class ChatViewModel : ViewModel() {
override fun onMessage(message: ImMessage) { override fun onMessage(message: ImMessage) {
if (isRelevant(message)) { if (isRelevant(message)) {
prependMessage(message) prependMessage(message)
cache.mergeHistory(targetId, chatType, _messages.value)
if (_searchQuery.value.isNotBlank()) {
_searchResults.value = cache.searchHistory(targetId, chatType, _searchQuery.value)
}
requestScrollToBottom() requestScrollToBottom()
} }
} }
@ -41,6 +53,10 @@ class ChatViewModel : ViewModel() {
override fun onGroupMessage(message: ImMessage) { override fun onGroupMessage(message: ImMessage) {
if (isRelevant(message)) { if (isRelevant(message)) {
prependMessage(message) prependMessage(message)
cache.mergeHistory(targetId, chatType, _messages.value)
if (_searchQuery.value.isNotBlank()) {
_searchResults.value = cache.searchHistory(targetId, chatType, _searchQuery.value)
}
requestScrollToBottom() requestScrollToBottom()
} }
} }
@ -52,9 +68,11 @@ class ChatViewModel : ViewModel() {
this.chatType = chatType this.chatType = chatType
nextHistoryPage = 0 nextHistoryPage = 0
initialized = true initialized = true
_messages.value = emptyList() _messages.value = cache.getHistoryPage(targetId, chatType, 0, HISTORY_PAGE_SIZE)
_hasMoreHistory.value = true _hasMoreHistory.value = true
_isLoadingMore.value = false _isLoadingMore.value = false
_searchQuery.value = ""
_searchResults.value = emptyList()
ImSDK.addListener(listener) ImSDK.addListener(listener)
loadInitialHistory() loadInitialHistory()
viewModelScope.launch { viewModelScope.launch {
@ -86,18 +104,31 @@ class ChatViewModel : ViewModel() {
nextHistoryPage += 1 nextHistoryPage += 1
} }
_hasMoreHistory.value = history.size >= HISTORY_PAGE_SIZE _hasMoreHistory.value = history.size >= HISTORY_PAGE_SIZE
if (history.isNotEmpty()) {
cache.saveHistory(targetId, chatType, mergeHistory(_messages.value))
}
_isLoadingMore.value = false _isLoadingMore.value = false
} }
} }
private suspend fun fetchHistory(page: Int): List<ImMessage> { private suspend fun fetchHistory(page: Int): List<ImMessage> {
return if (chatType == "GROUP") { val remote = if (chatType == "GROUP") {
runCatching { ImSDK.fetchGroupHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) } runCatching { ImSDK.fetchGroupHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) }
.getOrDefault(emptyList()) .getOrNull()
} else { } else {
runCatching { ImSDK.fetchHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) } runCatching { ImSDK.fetchHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) }
.getOrDefault(emptyList()) .getOrNull()
} }
if (remote != null) {
val normalized = remote.sortedByDescending { it.createdAt }
if (page == 0) {
cache.saveHistory(targetId, chatType, normalized)
} else {
cache.mergeHistory(targetId, chatType, normalized)
}
return normalized
}
return cache.getHistoryPage(targetId, chatType, page, HISTORY_PAGE_SIZE)
} }
fun sendText(content: String) { fun sendText(content: String) {
@ -105,11 +136,28 @@ class ChatViewModel : ViewModel() {
ImSDK.sendMessage(targetId, chatType, "TEXT", content) ImSDK.sendMessage(targetId, chatType, "TEXT", content)
} }
fun searchCachedMessages(query: String) {
_searchQuery.value = query
_searchResults.value = if (query.isBlank()) {
emptyList()
} else {
cache.searchHistory(targetId, chatType, query)
}
}
fun clearSearch() {
_searchQuery.value = ""
_searchResults.value = emptyList()
}
private fun prependMessage(message: ImMessage) { private fun prependMessage(message: ImMessage) {
if (_messages.value.any { it.id == message.id }) return if (_messages.value.any { it.id == message.id }) return
_messages.value = listOf(message) + _messages.value _messages.value = listOf(message) + _messages.value
} }
private fun mergeHistory(messages: List<ImMessage>): List<ImMessage> =
messages.distinctBy { it.id }.sortedByDescending { it.createdAt }
private fun requestScrollToBottom() { private fun requestScrollToBottom() {
_scrollToBottomSignal.value = _scrollToBottomSignal.value + 1 _scrollToBottomSignal.value = _scrollToBottomSignal.value + 1
} }

查看文件

@ -35,6 +35,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.xuqm.sdk.im.model.ConversationData import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.ui.InitialAvatar import com.xuqm.sdk.ui.InitialAvatar
import com.xuqm.sdk.ui.SearchBarField
import com.xuqm.sdk.utils.TimeFormatters import com.xuqm.sdk.utils.TimeFormatters
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,9 +46,39 @@ fun ConversationScreen(
) { ) {
val conversations by viewModel.conversations.collectAsStateWithLifecycle() val conversations by viewModel.conversations.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var query by remember { mutableStateOf("") }
val filtered = remember(conversations, query) {
conversations.filter { conversation ->
if (query.isBlank()) return@filter true
val keyword = query.trim()
conversation.targetId.contains(keyword, ignoreCase = true) ||
(conversation.lastMsgContent ?: "").contains(keyword, ignoreCase = true) ||
conversation.chatType.contains(keyword, ignoreCase = true)
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
items(conversations, key = { "${it.chatType}_${it.targetId}" }) { conv -> SearchBarField(
value = query,
onValueChange = { query = it },
placeholder = "搜索会话 / 最近消息",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
)
if (filtered.isEmpty()) {
Text(
"没有匹配的会话",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
items(filtered, key = { "${it.chatType}_${it.targetId}" }) { conv ->
ConversationItem( ConversationItem(
conversation = conv, conversation = conv,
onClick = { onOpenChat(conv.targetId, conv.chatType, conv.targetId) }, onClick = { onOpenChat(conv.targetId, conv.chatType, conv.targetId) },
@ -62,6 +93,8 @@ fun ConversationScreen(
} }
} }
} }
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable

查看文件

@ -6,12 +6,14 @@ import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ConversationData import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImMessage import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ConversationViewModel : ViewModel() { class ConversationViewModel : ViewModel() {
private val cache = AppDependencies.localImCache
private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList()) private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList())
val conversations: StateFlow<List<ConversationData>> = _conversations val conversations: StateFlow<List<ConversationData>> = _conversations
@ -22,6 +24,7 @@ class ConversationViewModel : ViewModel() {
init { init {
ImSDK.addListener(listener) ImSDK.addListener(listener)
_conversations.value = cache.loadConversations().sortedByDescending { it.lastMsgTime }
refresh() refresh()
} }
@ -29,7 +32,13 @@ class ConversationViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
runCatching { ImSDK.listConversations() } runCatching { ImSDK.listConversations() }
.onSuccess { list -> .onSuccess { list ->
_conversations.value = list.sortedByDescending { it.lastMsgTime } val sorted = list.sortedByDescending { it.lastMsgTime }
cache.saveConversations(sorted)
_conversations.value = sorted
}
.onFailure {
val cached = cache.loadConversations().sortedByDescending { it.lastMsgTime }
if (cached.isNotEmpty()) _conversations.value = cached
} }
} }
} }