mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
Limit geohash teleport participant cache
This commit is contained in:
@@ -7,8 +7,19 @@ final class LocationPresenceStore: ObservableObject {
|
|||||||
@Published private(set) var geoNicknames: [String: String] = [:]
|
@Published private(set) var geoNicknames: [String: String] = [:]
|
||||||
@Published private(set) var teleportedGeo: Set<String> = []
|
@Published private(set) var teleportedGeo: Set<String> = []
|
||||||
|
|
||||||
|
private let teleportedGeoCapacity: Int
|
||||||
|
private var teleportedGeoOrder: [String] = []
|
||||||
|
|
||||||
|
init(teleportedGeoCapacity: Int = TransportConfig.geoTeleportedParticipantsCap) {
|
||||||
|
self.teleportedGeoCapacity = max(0, teleportedGeoCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
func setCurrentGeohash(_ geohash: String?) {
|
func setCurrentGeohash(_ geohash: String?) {
|
||||||
currentGeohash = geohash?.lowercased()
|
let normalized = geohash?.lowercased()
|
||||||
|
if currentGeohash != normalized {
|
||||||
|
clearTeleportedGeo()
|
||||||
|
}
|
||||||
|
currentGeohash = normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNickname(_ nickname: String, for pubkeyHex: String) {
|
func setNickname(_ nickname: String, for pubkeyHex: String) {
|
||||||
@@ -28,24 +39,63 @@ final class LocationPresenceStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markTeleported(_ pubkeyHex: String) {
|
func markTeleported(_ pubkeyHex: String) {
|
||||||
teleportedGeo.insert(pubkeyHex.lowercased())
|
guard teleportedGeoCapacity > 0 else {
|
||||||
|
clearTeleportedGeo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = pubkeyHex.lowercased()
|
||||||
|
guard !teleportedGeo.contains(key) else { return }
|
||||||
|
|
||||||
|
while teleportedGeoOrder.count >= teleportedGeoCapacity, let oldest = teleportedGeoOrder.first {
|
||||||
|
teleportedGeoOrder.removeFirst()
|
||||||
|
teleportedGeo.remove(oldest)
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportedGeo.insert(key)
|
||||||
|
teleportedGeoOrder.append(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearTeleported(_ pubkeyHex: String) {
|
func clearTeleported(_ pubkeyHex: String) {
|
||||||
teleportedGeo.remove(pubkeyHex.lowercased())
|
let key = pubkeyHex.lowercased()
|
||||||
|
teleportedGeo.remove(key)
|
||||||
|
teleportedGeoOrder.removeAll { $0 == key }
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaceTeleportedGeo(_ pubkeys: Set<String>) {
|
func replaceTeleportedGeo(_ pubkeys: Set<String>) {
|
||||||
teleportedGeo = Set(pubkeys.map { $0.lowercased() })
|
guard teleportedGeoCapacity > 0 else {
|
||||||
|
clearTeleportedGeo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var seen: Set<String> = []
|
||||||
|
var ordered: [String] = []
|
||||||
|
for key in pubkeys.map({ $0.lowercased() }) where !seen.contains(key) {
|
||||||
|
seen.insert(key)
|
||||||
|
ordered.append(key)
|
||||||
|
}
|
||||||
|
if ordered.count > teleportedGeoCapacity {
|
||||||
|
ordered = Array(ordered.suffix(teleportedGeoCapacity))
|
||||||
|
}
|
||||||
|
teleportedGeoOrder = ordered
|
||||||
|
teleportedGeo = Set(ordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retainTeleportedGeo(keeping pubkeys: Set<String>) {
|
||||||
|
let allowed = Set(pubkeys.map { $0.lowercased() })
|
||||||
|
teleportedGeoOrder = teleportedGeoOrder.filter { allowed.contains($0) }
|
||||||
|
teleportedGeo = teleportedGeo.intersection(allowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearTeleportedGeo() {
|
func clearTeleportedGeo() {
|
||||||
teleportedGeo.removeAll()
|
teleportedGeo.removeAll()
|
||||||
|
teleportedGeoOrder.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
currentGeohash = nil
|
currentGeohash = nil
|
||||||
geoNicknames.removeAll()
|
geoNicknames.removeAll()
|
||||||
teleportedGeo.removeAll()
|
teleportedGeo.removeAll()
|
||||||
|
teleportedGeoOrder.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum TransportConfig {
|
|||||||
static let privateChatCap: Int = 1337
|
static let privateChatCap: Int = 1337
|
||||||
static let meshTimelineCap: Int = 1337
|
static let meshTimelineCap: Int = 1337
|
||||||
static let geoTimelineCap: Int = 1337
|
static let geoTimelineCap: Int = 1337
|
||||||
|
static let geoTeleportedParticipantsCap: Int = 1337
|
||||||
static let contentLRUCap: Int = 2000
|
static let contentLRUCap: Int = 2000
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ final class ChatNostrCoordinator {
|
|||||||
let hasTeleportTag = event.tags.contains { tag in
|
let hasTeleportTag = event.tags.contains { tag in
|
||||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
||||||
}
|
}
|
||||||
|
let content = event.content.trimmed
|
||||||
|
if hasTeleportTag && content.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if hasTeleportTag {
|
if hasTeleportTag {
|
||||||
let key = event.pubkey.lowercased()
|
let key = event.pubkey.lowercased()
|
||||||
@@ -111,7 +115,6 @@ final class ChatNostrCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let senderName = viewModel.displayNameForNostrPubkey(event.pubkey)
|
let senderName = viewModel.displayNameForNostrPubkey(event.pubkey)
|
||||||
let content = event.content.trimmed
|
|
||||||
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||||
let timestamp = min(rawTs, Date())
|
let timestamp = min(rawTs, Date())
|
||||||
let mentions = viewModel.parseMentions(from: content)
|
let mentions = viewModel.parseMentions(from: content)
|
||||||
@@ -272,6 +275,10 @@ final class ChatNostrCoordinator {
|
|||||||
let hasTeleportTag = event.tags.contains { tag in
|
let hasTeleportTag = event.tags.contains { tag in
|
||||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
||||||
}
|
}
|
||||||
|
let content = event.content
|
||||||
|
if hasTeleportTag && content.trimmed.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let isSelf: Bool = {
|
let isSelf: Bool = {
|
||||||
if let gh = viewModel.currentGeohash,
|
if let gh = viewModel.currentGeohash,
|
||||||
@@ -314,14 +321,6 @@ final class ChatNostrCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let senderName = viewModel.displayNameForNostrPubkey(event.pubkey)
|
let senderName = viewModel.displayNameForNostrPubkey(event.pubkey)
|
||||||
let content = event.content
|
|
||||||
|
|
||||||
if let teleTag = event.tags.first(where: { $0.first == "t" }),
|
|
||||||
teleTag.count >= 2,
|
|
||||||
teleTag[1] == "teleport",
|
|
||||||
content.trimmed.isEmpty {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||||
let mentions = viewModel.parseMentions(from: content)
|
let mentions = viewModel.parseMentions(from: content)
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ private extension ChatViewModelBootstrapper {
|
|||||||
viewModel?.objectWillChange.send()
|
viewModel?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.cancellables)
|
.store(in: &viewModel.cancellables)
|
||||||
|
|
||||||
|
viewModel.participantTracker.$visiblePeople
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak viewModel] people in
|
||||||
|
Task { @MainActor [weak viewModel] in
|
||||||
|
viewModel?.locationPresenceStore.retainTeleportedGeo(
|
||||||
|
keeping: Set(people.map { $0.id })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPersistedViewState() {
|
func loadPersistedViewState() {
|
||||||
|
|||||||
@@ -125,6 +125,26 @@ struct AppArchitectureTests {
|
|||||||
#expect(store.teleportedGeo.isEmpty)
|
#expect(store.teleportedGeo.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test("LocationPresenceStore bounds and prunes teleported geohash participants")
|
||||||
|
@MainActor
|
||||||
|
func locationPresenceStoreBoundsTeleportedParticipants() {
|
||||||
|
let store = LocationPresenceStore(teleportedGeoCapacity: 2)
|
||||||
|
|
||||||
|
store.setCurrentGeohash("u4pruy")
|
||||||
|
store.markTeleported("AAAAAA")
|
||||||
|
store.markTeleported("BBBBBB")
|
||||||
|
store.markTeleported("CCCCCC")
|
||||||
|
|
||||||
|
#expect(store.teleportedGeo == Set(["bbbbbb", "cccccc"]))
|
||||||
|
|
||||||
|
store.retainTeleportedGeo(keeping: Set(["CCCCCC"]))
|
||||||
|
#expect(store.teleportedGeo == Set(["cccccc"]))
|
||||||
|
|
||||||
|
store.setCurrentGeohash("u4pruz")
|
||||||
|
#expect(store.teleportedGeo.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
@Test("PeerHandle equality and hashing use the canonical identity only")
|
@Test("PeerHandle equality and hashing use the canonical identity only")
|
||||||
func peerHandleEqualityUsesCanonicalIdentity() {
|
func peerHandleEqualityUsesCanonicalIdentity() {
|
||||||
let first = PeerHandle(
|
let first = PeerHandle(
|
||||||
|
|||||||
Reference in New Issue
Block a user