From c29d20d5c51feb4ad8024e42551f3c6c8082db16 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 29 Aug 2026 18:29:36 +0530 Subject: [PATCH 1/2] Raise NIP-17 gift-wrap fetch limit from 100 to 1000 Each delivered-and-read DM is typically three gift wraps, and relays return the newest-by-created_at under the limit. With randomized timestamps, a cap of 100 permanently drops older envelopes inside the lookback (~33 messages). Align with the geohash fetch budget. --- app/src/main/java/com/bitchat/android/nostr/NostrFilter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 ) } From d74fb7d82cf2d4511c9f0c91402ca0db3e37ca39 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sun, 25 Oct 2026 05:17:09 +0530 Subject: [PATCH 2/2] fix: skip delivery-status updates that do not change the state A replayed read receipt was copying every private chat onto main even when the message was already read. --- .../com/bitchat/android/ui/MessageManager.kt | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) 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 31e7d1afb..50c725a62 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageManager.kt @@ -302,18 +302,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 @@ -333,27 +348,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) + } } // Remove a message from all locations (main timeline, private chats, channels)