feat(im): add remote chat search and locate
这个提交包含在:
父节点
65bdb352bf
当前提交
ce1301fd65
@ -93,6 +93,8 @@ fun ChatScreen(
|
||||
val mentionableUsers by viewModel.mentionableUsers.collectAsStateWithLifecycle()
|
||||
val replyTargetMessage by viewModel.replyTargetMessage.collectAsStateWithLifecycle()
|
||||
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
|
||||
val focusMessageId by viewModel.focusMessageId.collectAsStateWithLifecycle()
|
||||
val focusMessageSignal by viewModel.focusMessageSignal.collectAsStateWithLifecycle()
|
||||
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
|
||||
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
|
||||
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
|
||||
@ -195,6 +197,14 @@ fun ChatScreen(
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(focusMessageSignal, messages.size) {
|
||||
val messageId = focusMessageId ?: return@LaunchedEffect
|
||||
val index = messages.indexOfFirst { it.id == messageId }
|
||||
if (index >= 0) {
|
||||
listState.animateScrollToItem(index)
|
||||
viewModel.clearFocusedMessage()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(listState, hasMoreHistory, isLoadingMore, messages.size, searchQuery) {
|
||||
if (searchQuery.isNotBlank()) return@LaunchedEffect
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 }
|
||||
@ -428,8 +438,8 @@ fun ChatScreen(
|
||||
if (showSearchBar || searchQuery.isNotBlank()) {
|
||||
SearchBarField(
|
||||
value = searchQuery,
|
||||
onValueChange = viewModel::searchCachedMessages,
|
||||
placeholder = "搜索当前会话本地消息",
|
||||
onValueChange = viewModel::searchMessages,
|
||||
placeholder = "搜索当前会话消息",
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
@ -445,6 +455,7 @@ fun ChatScreen(
|
||||
message = msg,
|
||||
isOwn = msg.fromId == viewModel.currentUserId,
|
||||
currentUserId = viewModel.currentUserId,
|
||||
onClick = { viewModel.openSearchResult(msg.id) },
|
||||
onReply = viewModel::startReply,
|
||||
onEdit = viewModel::startEdit,
|
||||
)
|
||||
@ -475,6 +486,7 @@ fun ChatScreen(
|
||||
message = msg,
|
||||
isOwn = msg.fromId == viewModel.currentUserId,
|
||||
currentUserId = viewModel.currentUserId,
|
||||
onClick = {},
|
||||
onReply = viewModel::startReply,
|
||||
onEdit = viewModel::startEdit,
|
||||
)
|
||||
@ -533,6 +545,7 @@ private fun MessageBubble(
|
||||
message: ImMessage,
|
||||
isOwn: Boolean,
|
||||
currentUserId: String,
|
||||
onClick: () -> Unit,
|
||||
onReply: (ImMessage) -> Unit,
|
||||
onEdit: (ImMessage) -> Unit,
|
||||
) {
|
||||
@ -566,7 +579,7 @@ private fun MessageBubble(
|
||||
.widthIn(max = 280.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
if (isOwn && message.msgType.uppercase() == "TEXT") {
|
||||
onEdit(message)
|
||||
|
||||
@ -17,6 +17,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
@ -40,6 +42,12 @@ class ChatViewModel : ViewModel() {
|
||||
private val _searchResults = MutableStateFlow<List<ImMessage>>(emptyList())
|
||||
val searchResults: StateFlow<List<ImMessage>> = _searchResults
|
||||
|
||||
private val _focusMessageId = MutableStateFlow<String?>(null)
|
||||
val focusMessageId: StateFlow<String?> = _focusMessageId
|
||||
|
||||
private val _focusMessageSignal = MutableStateFlow(0L)
|
||||
val focusMessageSignal: StateFlow<Long> = _focusMessageSignal
|
||||
|
||||
private val _draftText = MutableStateFlow("")
|
||||
val draftText: StateFlow<String> = _draftText
|
||||
|
||||
@ -65,6 +73,7 @@ class ChatViewModel : ViewModel() {
|
||||
private var nextHistoryPage = 0
|
||||
private var initialized = false
|
||||
private val pendingDeliveryTimeouts = mutableMapOf<String, kotlinx.coroutines.Job>()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
private val listener = object : ImEventListener {
|
||||
override fun onMessage(message: ImMessage) {
|
||||
@ -229,12 +238,57 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun searchCachedMessages(query: String) {
|
||||
fun searchMessages(query: String) {
|
||||
_searchQuery.value = query
|
||||
_searchResults.value = if (query.isBlank()) {
|
||||
emptyList()
|
||||
} else {
|
||||
cache.searchHistory(targetId, chatType, query)
|
||||
searchJob?.cancel()
|
||||
if (query.isBlank() || !initialized) {
|
||||
_searchResults.value = emptyList()
|
||||
return
|
||||
}
|
||||
searchJob = viewModelScope.launch {
|
||||
delay(250)
|
||||
val normalized = query.trim()
|
||||
val remote = runCatching {
|
||||
if (chatType == "GROUP") {
|
||||
ImSDK.fetchGroupHistoryWithFilters(
|
||||
groupId = targetId,
|
||||
page = 0,
|
||||
size = SEARCH_PAGE_SIZE,
|
||||
keyword = normalized,
|
||||
)
|
||||
} else {
|
||||
ImSDK.fetchHistoryWithFilters(
|
||||
toId = targetId,
|
||||
page = 0,
|
||||
size = SEARCH_PAGE_SIZE,
|
||||
keyword = normalized,
|
||||
)
|
||||
}
|
||||
}.getOrNull()
|
||||
_searchResults.value = if (remote != null) {
|
||||
remote.sortedByDescending { it.createdAt }
|
||||
} else {
|
||||
cache.searchHistory(targetId, chatType, normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSearchResult(messageId: String) {
|
||||
if (!initialized) return
|
||||
viewModelScope.launch {
|
||||
val visible = _messages.value.any { it.id == messageId }
|
||||
if (!visible) {
|
||||
val located = locateMessagePage(messageId)
|
||||
if (located == null) {
|
||||
_events.tryEmit("未找到消息")
|
||||
return@launch
|
||||
}
|
||||
_messages.value = mergeMessages(_messages.value, located)
|
||||
cache.mergeHistory(targetId, chatType, _messages.value)
|
||||
}
|
||||
_focusMessageId.value = messageId
|
||||
_focusMessageSignal.value = _focusMessageSignal.value + 1
|
||||
clearSearch()
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,10 +371,16 @@ class ChatViewModel : ViewModel() {
|
||||
fun sendFile(uri: Uri) = sendAttachment(uri, AttachmentKind.FILE)
|
||||
|
||||
fun clearSearch() {
|
||||
searchJob?.cancel()
|
||||
searchJob = null
|
||||
_searchQuery.value = ""
|
||||
_searchResults.value = emptyList()
|
||||
}
|
||||
|
||||
fun clearFocusedMessage() {
|
||||
_focusMessageId.value = null
|
||||
}
|
||||
|
||||
private fun mergeHistory(messages: List<ImMessage>): List<ImMessage> =
|
||||
messages.distinctBy { it.id }.sortedByDescending { it.createdAt }
|
||||
|
||||
@ -485,7 +545,7 @@ class ChatViewModel : ViewModel() {
|
||||
)
|
||||
AppDependencies.notifyConversationChanged()
|
||||
if (_searchQuery.value.isNotBlank()) {
|
||||
_searchResults.value = cache.searchHistory(targetId, chatType, _searchQuery.value)
|
||||
searchMessages(_searchQuery.value)
|
||||
}
|
||||
requestScrollToBottom()
|
||||
if (message.status.uppercase() != "READ" && message.fromId != currentUserId) {
|
||||
@ -577,8 +637,20 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun locateMessagePage(messageId: String): List<ImMessage>? {
|
||||
return runCatching {
|
||||
if (chatType == "GROUP") {
|
||||
ImSDK.locateGroupHistoryPage(targetId, messageId, HISTORY_PAGE_SIZE, LOCATE_MAX_PAGES)
|
||||
} else {
|
||||
ImSDK.locateHistoryPage(targetId, messageId, HISTORY_PAGE_SIZE, LOCATE_MAX_PAGES)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "XuqmChatViewModel"
|
||||
const val HISTORY_PAGE_SIZE = 20
|
||||
const val SEARCH_PAGE_SIZE = 50
|
||||
const val LOCATE_MAX_PAGES = 30
|
||||
}
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户