mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Resolve merge conflicts in ChatViewModel.swift (geohash sampling comments and removed background nudge)
This commit is contained in:
@@ -50,10 +50,8 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
||||
let arr = try? JSONDecoder().decode([String].self, from: data) {
|
||||
teleportedSet = Set(arr)
|
||||
}
|
||||
// Initialize teleported flag from persisted state if a location channel is selected
|
||||
if case .location(let ch) = selectedChannel {
|
||||
teleported = teleportedSet.contains(ch.geohash)
|
||||
}
|
||||
// Do not eagerly mark teleported on startup; wait for location to compute regional set.
|
||||
// This avoids showing teleported for in-region channels during cold start.
|
||||
let status: CLAuthorizationStatus
|
||||
if #available(iOS 14.0, macOS 11.0, *) {
|
||||
status = cl.authorizationStatus
|
||||
@@ -61,6 +59,15 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
||||
status = CLLocationManager.authorizationStatus()
|
||||
}
|
||||
updatePermissionState(from: status)
|
||||
// If we don't have location authorization at startup, fall back to persisted teleport state
|
||||
switch status {
|
||||
case .authorizedAlways, .authorizedWhenInUse, .authorized:
|
||||
break // will compute from location
|
||||
default:
|
||||
if case .location(let ch) = selectedChannel {
|
||||
teleported = teleportedSet.contains(ch.geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
@@ -129,7 +136,21 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
||||
case .mesh:
|
||||
self.teleported = false
|
||||
case .location(let ch):
|
||||
self.teleported = self.teleportedSet.contains(ch.geohash)
|
||||
// If this geohash is in our current regional set, do NOT mark teleported.
|
||||
let inRegional = self.availableChannels.contains { $0.geohash == ch.geohash }
|
||||
if inRegional {
|
||||
self.teleported = false
|
||||
// Clear persisted teleport for this geohash to keep future selections clean
|
||||
if self.teleportedSet.contains(ch.geohash) {
|
||||
self.teleportedSet.remove(ch.geohash)
|
||||
if let data = try? JSONEncoder().encode(Array(self.teleportedSet)) {
|
||||
UserDefaults.standard.set(data, forKey: self.teleportedStoreKey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fall back to persisted mark (set by deep link or manual teleport)
|
||||
self.teleported = self.teleportedSet.contains(ch.geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,14 +223,25 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
||||
}
|
||||
Task { @MainActor in
|
||||
self.availableChannels = result
|
||||
// Recompute teleported status based on persisted state OR current location vs selected channel
|
||||
// Recompute teleported status based on whether the selected geohash is in our regional set
|
||||
switch self.selectedChannel {
|
||||
case .mesh:
|
||||
self.teleported = false
|
||||
case .location(let ch):
|
||||
let persisted = self.teleportedSet.contains(ch.geohash)
|
||||
let currentGH = Geohash.encode(latitude: coord.latitude, longitude: coord.longitude, precision: ch.level.precision)
|
||||
self.teleported = persisted || (currentGH != ch.geohash)
|
||||
// Membership check using freshly computed regional channels; avoids precision/rename drift
|
||||
let inRegional = result.contains { $0.geohash == ch.geohash }
|
||||
if inRegional {
|
||||
self.teleported = false
|
||||
// Clear persisted teleport flag if present
|
||||
if self.teleportedSet.contains(ch.geohash) {
|
||||
self.teleportedSet.remove(ch.geohash)
|
||||
if let data = try? JSONEncoder().encode(Array(self.teleportedSet)) {
|
||||
UserDefaults.standard.set(data, forKey: self.teleportedStoreKey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.teleported = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +659,26 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
if state == .authorized { LocationChannelManager.shared.refreshChannels() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Track teleport flag changes to keep our own teleported marker in sync with regional status
|
||||
LocationChannelManager.shared.$teleported
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isTeleported in
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor in
|
||||
guard case .location(let ch) = self.activeChannel,
|
||||
let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) else { return }
|
||||
let key = id.publicKeyHex.lowercased()
|
||||
let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty
|
||||
let inRegional = LocationChannelManager.shared.availableChannels.contains { $0.geohash == ch.geohash }
|
||||
if isTeleported && hasRegional && !inRegional {
|
||||
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||
} else {
|
||||
self.teleportedGeo.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Request notification permission
|
||||
NotificationService.shared.requestAuthorization()
|
||||
@@ -797,10 +817,19 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
})
|
||||
if hasTeleportTag {
|
||||
let key = event.pubkey.lowercased()
|
||||
Task { @MainActor in self.teleportedGeo = self.teleportedGeo.union([key]) }
|
||||
// Do not mark our own key from historical events; rely on manager.teleported for self
|
||||
let isSelf: Bool = {
|
||||
if let gh = self.currentGeohash, let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh) {
|
||||
return my.publicKeyHex.lowercased() == key
|
||||
}
|
||||
return false
|
||||
}()
|
||||
if !isSelf {
|
||||
Task { @MainActor in self.teleportedGeo = self.teleportedGeo.union([key]) }
|
||||
}
|
||||
}
|
||||
let senderName = self.displayNameForNostrPubkey(event.pubkey)
|
||||
let content = event.content
|
||||
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
let mentions = self.parseMentions(from: content)
|
||||
let msg = BitchatMessage(
|
||||
@@ -1242,7 +1271,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
/// Routes to private chat if one is selected, otherwise broadcasts
|
||||
@MainActor
|
||||
func sendMessage(_ content: String) {
|
||||
guard !content.isEmpty else { return }
|
||||
// Ignore messages that are empty or whitespace-only to prevent blank lines
|
||||
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
|
||||
// Check for commands
|
||||
if content.hasPrefix("/") {
|
||||
@@ -1262,7 +1293,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// Parse mentions from the content
|
||||
// Parse mentions from the content (use original content for user intent)
|
||||
let mentions = parseMentions(from: content)
|
||||
|
||||
// Add message to local display
|
||||
@@ -1277,7 +1308,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
let message = BitchatMessage(
|
||||
sender: displaySender,
|
||||
content: content,
|
||||
content: trimmed,
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
@@ -1322,7 +1353,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
do {
|
||||
let identity = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
||||
let event = try NostrProtocol.createEphemeralGeohashEvent(
|
||||
content: content,
|
||||
content: trimmed,
|
||||
geohash: ch.geohash,
|
||||
senderIdentity: identity,
|
||||
nickname: self.nickname,
|
||||
@@ -1342,7 +1373,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
SecureLogger.log("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
// If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI
|
||||
if LocationChannelManager.shared.teleported {
|
||||
// Only when not in our regional set (and regional list is known)
|
||||
let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty
|
||||
let inRegional = LocationChannelManager.shared.availableChannels.contains { $0.geohash == ch.geohash }
|
||||
if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
|
||||
let key = identity.publicKeyHex.lowercased()
|
||||
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||
SecureLogger.log("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
|
||||
@@ -1379,9 +1413,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
case .location(let ch):
|
||||
// Sanitize existing timeline (filter any prior empty-content entries)
|
||||
var arr = geoTimelines[ch.geohash] ?? []
|
||||
let before = arr.count
|
||||
arr.removeAll { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||
if arr.count != before { geoTimelines[ch.geohash] = arr }
|
||||
// Ensure chronological order when returning to a geohash
|
||||
if arr.count > 1 {
|
||||
arr.sort { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
// Persist the cleaned/sorted timeline for this geohash
|
||||
geoTimelines[ch.geohash] = arr
|
||||
messages = arr
|
||||
}
|
||||
// Unsubscribe previous
|
||||
@@ -1400,14 +1438,18 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
guard case .location(let ch) = channel else { return }
|
||||
currentGeohash = ch.geohash
|
||||
// Ensure self appears immediately in the people list; mark teleported state if applicable
|
||||
// Ensure self appears immediately in the people list; mark teleported state only when truly teleported
|
||||
if let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
||||
self.recordGeoParticipant(pubkeyHex: id.publicKeyHex)
|
||||
if LocationChannelManager.shared.teleported {
|
||||
let key = id.publicKeyHex.lowercased()
|
||||
let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty
|
||||
let inRegional = LocationChannelManager.shared.availableChannels.contains { $0.geohash == ch.geohash }
|
||||
let key = id.publicKeyHex.lowercased()
|
||||
if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
|
||||
teleportedGeo = teleportedGeo.union([key])
|
||||
SecureLogger.log("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)",
|
||||
category: SecureLogger.session, level: .info)
|
||||
} else {
|
||||
teleportedGeo.remove(key)
|
||||
}
|
||||
}
|
||||
let subID = "geo-\(ch.geohash)"
|
||||
@@ -1436,10 +1478,19 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
})
|
||||
if hasTeleportTag {
|
||||
let key = event.pubkey.lowercased()
|
||||
Task { @MainActor in
|
||||
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||
SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
|
||||
category: SecureLogger.session, level: .info)
|
||||
// Avoid marking our own key from historical events; rely on manager.teleported for self
|
||||
let isSelf: Bool = {
|
||||
if let gh = self.currentGeohash, let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh) {
|
||||
return my.publicKeyHex.lowercased() == key
|
||||
}
|
||||
return false
|
||||
}()
|
||||
if !isSelf {
|
||||
Task { @MainActor in
|
||||
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||
SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
|
||||
category: SecureLogger.session, level: .info)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip only very recent self-echo from relay; include older self events for hydration
|
||||
@@ -1791,7 +1842,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
||||
guard let self = self else { return }
|
||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||
// Compute existing participant count (5-minute window) BEFORE updating
|
||||
// Compute current participant count (5-minute window) BEFORE updating with this event
|
||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
||||
let existingCount: Int = {
|
||||
let map = self.geoParticipants[gh] ?? [:]
|
||||
@@ -1799,7 +1850,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}()
|
||||
// Update participants for this specific geohash
|
||||
self.recordGeoParticipant(pubkeyHex: event.pubkey, geohash: gh)
|
||||
// Notify only on rising-edge: previously zero people, now someone chats
|
||||
// Notify only on rising-edge: previously zero people, now someone sends a chat
|
||||
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !content.isEmpty else { return }
|
||||
// Respect geohash blocks
|
||||
@@ -4347,11 +4398,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
||||
Task { @MainActor in
|
||||
let publicMentions = parseMentions(from: content)
|
||||
let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let publicMentions = parseMentions(from: normalized)
|
||||
let msg = BitchatMessage(
|
||||
id: UUID().uuidString,
|
||||
sender: nickname,
|
||||
content: content,
|
||||
content: normalized,
|
||||
timestamp: timestamp,
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
|
||||
@@ -460,7 +460,13 @@ struct ContentView: View {
|
||||
}
|
||||
let level = levelForLength(gh.count)
|
||||
let ch = GeohashChannel(level: level, geohash: gh)
|
||||
LocationChannelManager.shared.markTeleported(for: gh, true)
|
||||
// Do not mark teleported when opening a geohash that is in our regional set.
|
||||
// If availableChannels is empty (e.g., cold start), defer marking and let
|
||||
// LocationChannelManager compute teleported based on actual location.
|
||||
let inRegional = LocationChannelManager.shared.availableChannels.contains { $0.geohash == gh }
|
||||
if !inRegional && !LocationChannelManager.shared.availableChannels.isEmpty {
|
||||
LocationChannelManager.shared.markTeleported(for: gh, true)
|
||||
}
|
||||
LocationChannelManager.shared.select(ChannelID.location(ch))
|
||||
}
|
||||
.onTapGesture(count: 3) {
|
||||
|
||||
Reference in New Issue
Block a user