From 793b21545776a2e70809fbf7ff7355a53f9e9684 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:07:59 +0300 Subject: [PATCH] fix: geohash online count accuracy and channel switch latency (#672) - Keep max lastSeen per user so relay's newest-first delivery doesn't overwrite a fresh timestamp with a stale one - Serialize event processing on Main dispatcher to eliminate ConcurrentModificationException on geohashParticipants maps - Immediately refresh people list on channel switch so sampling data is reflected before the first relay response arrives --- .../bitchat/android/nostr/GeohashMessageHandler.kt | 9 ++------- .../com/bitchat/android/nostr/GeohashRepository.kt | 11 ++++++++++- .../java/com/bitchat/android/ui/GeohashViewModel.kt | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt index b318ef69..1c4896f3 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt @@ -6,9 +6,7 @@ import com.bitchat.android.model.BitchatMessage import com.bitchat.android.ui.ChatState import com.bitchat.android.ui.MessageManager import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.Date /** @@ -44,7 +42,7 @@ class GeohashMessageHandler( } fun onEvent(event: NostrEvent, subscribedGeohash: String) { - scope.launch(Dispatchers.Default) { + scope.launch { try { if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1) @@ -65,9 +63,6 @@ class GeohashMessageHandler( // Blocked users check (use injected DataManager which has loaded state) if (dataManager.isGeohashUserBlocked(pubkey)) return@launch - // Update repository (participants, nickname, teleport) - // Update repository on a background-safe path; repository will post updates to LiveData - // Update participant count (last seen) on BOTH Presence (20001) and Chat (20000) events if (event.kind == NostrKind.GEOHASH_PRESENCE || event.kind == NostrKind.EPHEMERAL_EVENT) { repo.updateParticipant(subscribedGeohash, pubkey, Date(event.createdAt * 1000L)) @@ -107,7 +102,7 @@ class GeohashMessageHandler( if (hasNonce) NostrProofOfWork.calculateDifficulty(event.id).takeIf { it > 0 } else null } catch (_: Exception) { null } ) - withContext(Dispatchers.Main) { messageManager.addChannelMessage("geo:$subscribedGeohash", msg) } + messageManager.addChannelMessage("geo:$subscribedGeohash", msg) } catch (e: Exception) { Log.e(TAG, "onEvent error: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt index f6fbca78..3a7ced32 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt @@ -103,7 +103,16 @@ class GeohashRepository( fun updateParticipant(geohash: String, participantId: String, lastSeen: Date) { val participants = geohashParticipants.getOrPut(geohash) { mutableMapOf() } - participants[participantId] = lastSeen + // Cap to now: prevents future-timestamped events (clock skew / malicious created_at) + // from pinning lastSeen and blocking subsequent normal heartbeats. + // Also keeps max: relays send events newest-first, so subsequent older events for + // the same user must not overwrite a fresher lastSeen. + val now = Date() + val effective = if (lastSeen.after(now)) now else lastSeen + val existing = participants[participantId] + if (existing == null || effective.after(existing)) { + participants[participantId] = effective + } if (currentGeohash == geohash) refreshGeohashPeople() updateReactiveParticipantCounts() } diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt index b24ddc0e..0504aa11 100644 --- a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt @@ -367,6 +367,7 @@ class GeohashViewModel( Log.d(TAG, "📍 Switching to geohash channel: ${channel.channel.geohash}") activeChannelGeohash = channel.channel.geohash repo.setCurrentGeohash(channel.channel.geohash) + repo.refreshGeohashPeople() notificationManager.setCurrentGeohash(channel.channel.geohash) notificationManager.clearNotificationsForGeohash(channel.channel.geohash) try { messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}") } catch (_: Exception) { }