Recover stale Android chat ids
This commit is contained in:
@@ -22,8 +22,8 @@ android {
|
|||||||
applicationId = "xyz.kusoft.qmax"
|
applicationId = "xyz.kusoft.qmax"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 47
|
versionCode = 48
|
||||||
versionName = "0.1.46"
|
versionName = "0.1.47"
|
||||||
|
|
||||||
buildConfigField("String", "QMAX_DEFAULT_SERVER_URL", "\"https://qmax.kusoft.xyz\"")
|
buildConfigField("String", "QMAX_DEFAULT_SERVER_URL", "\"https://qmax.kusoft.xyz\"")
|
||||||
buildConfigField("String", "QMAX_DEFAULT_PAIRING_CODE", "\"qmax-MxRq4h2HQBEIFs6k\"")
|
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.MessageDeletedDto
|
||||||
import xyz.kusoft.qmax.core.model.MessageDto
|
import xyz.kusoft.qmax.core.model.MessageDto
|
||||||
import xyz.kusoft.qmax.core.model.QMaxSession
|
import xyz.kusoft.qmax.core.model.QMaxSession
|
||||||
|
import xyz.kusoft.qmax.core.network.QMaxHttpException
|
||||||
import xyz.kusoft.qmax.core.realtime.QMaxRealtimeClient
|
import xyz.kusoft.qmax.core.realtime.QMaxRealtimeClient
|
||||||
import java.io.File
|
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()) {
|
if (cached.isNotEmpty() && state.value.selectedChat?.id == chat.id && state.value.messages.isEmpty()) {
|
||||||
state.value = state.value.copy(messages = cached)
|
state.value = state.value.copy(messages = cached)
|
||||||
}
|
}
|
||||||
val fresh = withTimeout(MessageSyncTimeoutMs) {
|
val fresh = try {
|
||||||
repository.messages(session, chat.id)
|
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) {
|
if (state.value.selectedChat?.id == chat.id) {
|
||||||
val selected = state.value.selectedChat?.copy(unreadCount = 0)
|
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 sendMessage() = sendComposer(emptyList())
|
||||||
|
|
||||||
fun sendComposer(attachmentUris: List<Uri>, onSuccess: () -> Unit = {}) = launchLoading(false) {
|
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) {
|
private suspend fun refreshPresence(chatId: String) {
|
||||||
val session = state.value.session ?: return
|
val session = state.value.session ?: return
|
||||||
val presence = runCatching {
|
val presence = runCatching {
|
||||||
|
|||||||
Reference in New Issue
Block a user