Recover stale Android chat ids

This commit is contained in:
sevenhill
2026-07-04 14:17:28 +03:00
parent a7524e7928
commit 7c3c8d11bc
2 changed files with 94 additions and 4 deletions
+2 -2
View File
@@ -22,8 +22,8 @@ android {
applicationId = "xyz.kusoft.qmax"
minSdk = 26
targetSdk = 36
versionCode = 47
versionName = "0.1.46"
versionCode = 48
versionName = "0.1.47"
buildConfigField("String", "QMAX_DEFAULT_SERVER_URL", "\"https://qmax.kusoft.xyz\"")
buildConfigField("String", "QMAX_DEFAULT_PAIRING_CODE", "\"qmax-MxRq4h2HQBEIFs6k\"")
@@ -20,6 +20,7 @@ import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
import xyz.kusoft.qmax.core.model.MessageDeletedDto
import xyz.kusoft.qmax.core.model.MessageDto
import xyz.kusoft.qmax.core.model.QMaxSession
import xyz.kusoft.qmax.core.network.QMaxHttpException
import xyz.kusoft.qmax.core.realtime.QMaxRealtimeClient
import java.io.File
@@ -378,8 +379,15 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
if (cached.isNotEmpty() && state.value.selectedChat?.id == chat.id && state.value.messages.isEmpty()) {
state.value = state.value.copy(messages = cached)
}
val fresh = withTimeout(MessageSyncTimeoutMs) {
repository.messages(session, chat.id)
val fresh = try {
withTimeout(MessageSyncTimeoutMs) {
repository.messages(session, chat.id)
}
} catch (error: QMaxHttpException) {
if (error.code == 404 && recoverMissingChat(session, chat)) {
return@launchLoading
}
throw error
}
if (state.value.selectedChat?.id == chat.id) {
val selected = state.value.selectedChat?.copy(unreadCount = 0)
@@ -393,6 +401,61 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
}
}
private suspend fun recoverMissingChat(session: QMaxSession, staleChat: ChatDto): Boolean {
val freshChats = runCatching {
withTimeout(ChatListTimeoutMs) {
repository.chats(session)
}
}.getOrElse {
return false
}
val orderedFresh = normalizeChats(freshChats)
val replacement = findReplacementChat(staleChat, orderedFresh)
if (replacement == null) {
state.value = state.value.copy(
selectedChat = null,
chats = orderedFresh,
messages = emptyList(),
error = null
)
repository.cacheChats(session, orderedFresh)
return true
}
val openedChat = replacement.copy(unreadCount = 0)
rememberChatRead(openedChat)
val chats = normalizeChats(
orderedFresh.map { chat ->
if (chat.id == openedChat.id) chat.copy(unreadCount = 0) else chat
},
selectedChatId = openedChat.id
)
state.value = state.value.copy(
selectedChat = openedChat,
chats = chats,
messages = emptyList(),
chatPresenceText = null,
error = null
)
repository.cacheChats(session, chats)
syncChatRead(openedChat.id)
realtime.joinChat(openedChat.id)
val freshMessages = withTimeout(MessageSyncTimeoutMs) {
repository.messages(session, openedChat.id)
}
if (state.value.selectedChat?.id == openedChat.id) {
state.value = state.value.copy(
selectedChat = state.value.selectedChat?.copy(unreadCount = 0),
chats = normalizeChats(state.value.chats, selectedChatId = openedChat.id),
messages = freshMessages
)
persistCurrentChats()
}
return true
}
fun sendMessage() = sendComposer(emptyList())
fun sendComposer(attachmentUris: List<Uri>, onSuccess: () -> Unit = {}) = launchLoading(false) {
@@ -731,6 +794,33 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
}
}
private fun findReplacementChat(staleChat: ChatDto, freshChats: List<ChatDto>): ChatDto? {
staleChat.externalId?.takeIf { it.isNotBlank() }?.let { externalId ->
freshChats.firstOrNull { it.externalId == externalId }?.let { return it }
}
val titleMatches = freshChats.filter { it.title.equals(staleChat.title, ignoreCase = true) }
if (titleMatches.size == 1) {
return titleMatches.first()
}
val staleAvatar = staleChat.avatarUrl.orEmpty()
if (staleAvatar.isNotBlank()) {
titleMatches.firstOrNull { it.avatarUrl.orEmpty() == staleAvatar }?.let { return it }
val staleFingerprint = avatarFingerprint(staleAvatar)
titleMatches.firstOrNull { avatarFingerprint(it.avatarUrl.orEmpty()) == staleFingerprint }?.let { return it }
}
return null
}
private fun avatarFingerprint(value: String): String {
if (value.isBlank()) return ""
return runCatching {
android.net.Uri.parse(value).getQueryParameter("r")
}.getOrNull()?.takeIf { it.isNotBlank() } ?: value
}
private suspend fun refreshPresence(chatId: String) {
val session = state.value.session ?: return
val presence = runCatching {