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
This commit is contained in:
Moe Hamade
2026-06-25 22:07:59 +02:00
committed by GitHub
parent f0d312827e
commit 793b215457
3 changed files with 13 additions and 8 deletions
@@ -6,9 +6,7 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.ChatState import com.bitchat.android.ui.ChatState
import com.bitchat.android.ui.MessageManager import com.bitchat.android.ui.MessageManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date import java.util.Date
/** /**
@@ -44,7 +42,7 @@ class GeohashMessageHandler(
} }
fun onEvent(event: NostrEvent, subscribedGeohash: String) { fun onEvent(event: NostrEvent, subscribedGeohash: String) {
scope.launch(Dispatchers.Default) { scope.launch {
try { try {
if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch 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) 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) // Blocked users check (use injected DataManager which has loaded state)
if (dataManager.isGeohashUserBlocked(pubkey)) return@launch 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 // Update participant count (last seen) on BOTH Presence (20001) and Chat (20000) events
if (event.kind == NostrKind.GEOHASH_PRESENCE || event.kind == NostrKind.EPHEMERAL_EVENT) { if (event.kind == NostrKind.GEOHASH_PRESENCE || event.kind == NostrKind.EPHEMERAL_EVENT) {
repo.updateParticipant(subscribedGeohash, pubkey, Date(event.createdAt * 1000L)) 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 if (hasNonce) NostrProofOfWork.calculateDifficulty(event.id).takeIf { it > 0 } else null
} catch (_: Exception) { null } } catch (_: Exception) { null }
) )
withContext(Dispatchers.Main) { messageManager.addChannelMessage("geo:$subscribedGeohash", msg) } messageManager.addChannelMessage("geo:$subscribedGeohash", msg)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "onEvent error: ${e.message}") Log.e(TAG, "onEvent error: ${e.message}")
} }
@@ -103,7 +103,16 @@ class GeohashRepository(
fun updateParticipant(geohash: String, participantId: String, lastSeen: Date) { fun updateParticipant(geohash: String, participantId: String, lastSeen: Date) {
val participants = geohashParticipants.getOrPut(geohash) { mutableMapOf() } 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() if (currentGeohash == geohash) refreshGeohashPeople()
updateReactiveParticipantCounts() updateReactiveParticipantCounts()
} }
@@ -367,6 +367,7 @@ class GeohashViewModel(
Log.d(TAG, "📍 Switching to geohash channel: ${channel.channel.geohash}") Log.d(TAG, "📍 Switching to geohash channel: ${channel.channel.geohash}")
activeChannelGeohash = channel.channel.geohash activeChannelGeohash = channel.channel.geohash
repo.setCurrentGeohash(channel.channel.geohash) repo.setCurrentGeohash(channel.channel.geohash)
repo.refreshGeohashPeople()
notificationManager.setCurrentGeohash(channel.channel.geohash) notificationManager.setCurrentGeohash(channel.channel.geohash)
notificationManager.clearNotificationsForGeohash(channel.channel.geohash) notificationManager.clearNotificationsForGeohash(channel.channel.geohash)
try { messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}") } catch (_: Exception) { } try { messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}") } catch (_: Exception) { }