diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrFilter.kt b/app/src/main/java/com/bitchat/android/nostr/NostrFilter.kt index df67822f7..3f1c54fff 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrFilter.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrFilter.kt @@ -23,11 +23,15 @@ data class NostrFilter( * Create filter for NIP-17 gift wraps */ fun giftWrapsFor(pubkey: String, since: Long? = null): NostrFilter { + // Each delivered+read DM is typically three gift wraps (message, delivery + // ack, read receipt). A limit of 100 therefore covers only ~33 messages + // inside the lookback, and NIP-01 returns the newest-by-created_at which + // is randomized — so loss is permanent. Match the geohash fetch budget. return NostrFilter( kinds = listOf(NostrKind.GIFT_WRAP), since = since?.let { (it / 1000).toInt() }, tagFilters = mapOf("p" to listOf(pubkey)), - limit = 100 + limit = 1000 ) } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageManager.kt b/app/src/main/java/com/bitchat/android/ui/MessageManager.kt index ea3e8350e..bbd9446df 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageManager.kt @@ -289,18 +289,33 @@ class MessageManager(private val state: ChatState) { } } + /** + * A replayed receipt is the same state even when `Read`/`Delivered` carry + * a new timestamp. Rewriting the chat list for that copies every thread + * onto main during a cold-start backlog. + */ + private fun isUnchangedDelivery(current: DeliveryStatus?, updated: DeliveryStatus?): Boolean { + if (current === updated || current == updated) return true + return when { + current is DeliveryStatus.Read && updated is DeliveryStatus.Read -> true + current is DeliveryStatus.Delivered && updated is DeliveryStatus.Delivered -> true + else -> false + } + } + fun updateMessageDeliveryStatus(messageID: String, status: DeliveryStatus) { - // Update in private chats + // Update in private chats. Copy a thread only when one of its messages + // actually changes, so a no-op receipt does not rebuild the map. val updatedPrivateChats = state.getPrivateChatsValue().toMutableMap() var updated = false - + updatedPrivateChats.forEach { (peerID, messages) -> - val updatedMessages = messages.toMutableList() - val messageIndex = updatedMessages.indexOfFirst { it.id == messageID } + val messageIndex = messages.indexOfFirst { it.id == messageID } if (messageIndex >= 0) { - val current = updatedMessages[messageIndex].deliveryStatus + val current = messages[messageIndex].deliveryStatus val finalStatus = chooseStatus(current, status) - if (finalStatus !== current) { + if (!isUnchangedDelivery(current, finalStatus)) { + val updatedMessages = messages.toMutableList() updatedMessages[messageIndex] = updatedMessages[messageIndex].copy(deliveryStatus = finalStatus) updatedPrivateChats[peerID] = updatedMessages updated = true @@ -320,27 +335,31 @@ class MessageManager(private val state: ChatState) { if (messageIndex >= 0) { val current = updatedMessages[messageIndex].deliveryStatus val finalStatus = chooseStatus(current, status) - if (finalStatus !== current) { + if (!isUnchangedDelivery(current, finalStatus)) { updatedMessages[messageIndex] = updatedMessages[messageIndex].copy(deliveryStatus = finalStatus) state.setMessages(updatedMessages) } } - + // Update in channel messages val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap() + var channelUpdated = false updatedChannelMessages.forEach { (channel, messages) -> - val channelMessagesList = messages.toMutableList() - val channelMessageIndex = channelMessagesList.indexOfFirst { it.id == messageID } + val channelMessageIndex = messages.indexOfFirst { it.id == messageID } if (channelMessageIndex >= 0) { - val current = channelMessagesList[channelMessageIndex].deliveryStatus + val current = messages[channelMessageIndex].deliveryStatus val finalStatus = chooseStatus(current, status) - if (finalStatus !== current) { + if (!isUnchangedDelivery(current, finalStatus)) { + val channelMessagesList = messages.toMutableList() channelMessagesList[channelMessageIndex] = channelMessagesList[channelMessageIndex].copy(deliveryStatus = finalStatus) updatedChannelMessages[channel] = channelMessagesList + channelUpdated = true } } } - state.setChannelMessages(updatedChannelMessages) + if (channelUpdated) { + state.setChannelMessages(updatedChannelMessages) + } } // Drop a blocked sender's messages from every geohash timeline