mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
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:
@@ -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) { }
|
||||||
|
|||||||
Reference in New Issue
Block a user