Compare commits

...
4 changed files with 40 additions and 39 deletions
@@ -13,12 +13,16 @@ final class FavoritesPersistenceService: ObservableObject {
let theyFavoritedUs: Bool let theyFavoritedUs: Bool
let favoritedAt: Date let favoritedAt: Date
let lastUpdated: Date let lastUpdated: Date
// Track what we last sent as OUR npub to this peer, to avoid resending unless it changes
// Note: we do not track which npub we last sent to them; sending happens only on favorite toggle
var isMutual: Bool { var isMutual: Bool {
isFavorite && theyFavoritedUs isFavorite && theyFavoritedUs
} }
} }
// We intentionally do not track when we last sent our npub; sending happens only on favorite toggle.
private static let storageKey = "chat.bitchat.favorites" private static let storageKey = "chat.bitchat.favorites"
private static let keychainService = "chat.bitchat.favorites" private static let keychainService = "chat.bitchat.favorites"
+1
View File
@@ -74,6 +74,7 @@ final class MessageRouter {
} }
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
// Route via mesh when connected; else use Nostr
if mesh.isPeerConnected(peerID) { if mesh.isPeerConnected(peerID) {
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
} else { } else {
-21
View File
@@ -195,27 +195,6 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
let favoriteStatus = favorites[noiseKey] { let favoriteStatus = favorites[noiseKey] {
peer.favoriteStatus = favoriteStatus peer.favoriteStatus = favoriteStatus
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
} else {
// Check by nickname for reconnected peers
let favoriteByNickname = favorites.values.first {
$0.peerNickname == peerInfo.nickname
}
if let favorite = favoriteByNickname,
let noiseKey = peerInfo.noisePublicKey {
SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)
// Update the favorite's key in persistence
favoritesService.updateNoisePublicKey(
from: favorite.peerNoisePublicKey,
to: noiseKey,
peerNickname: peerInfo.nickname
)
// Get updated favorite
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
}
} }
return peer return peer
+35 -18
View File
@@ -420,6 +420,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
// MARK: - Message Delivery Tracking // MARK: - Message Delivery Tracking
// Delivery tracking // Delivery tracking
@@ -4599,16 +4600,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Register ephemeral session with identity manager // Register ephemeral session with identity manager
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID) SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
// Check if we favorite this peer and resend notification on reconnect // Intentionally do not resend favorites on reconnect.
// This ensures Nostr key mapping is maintained across reconnections // We only send our npub when a favorite is toggled on, or if our npub changes.
if let peer = unifiedPeerService.getPeer(by: peerID),
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
favoriteStatus.isFavorite {
// Resend favorite notification with our Nostr key after a short delay
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
SecureLogger.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session)
}
// Force UI refresh // Force UI refresh
objectWillChange.send() objectWillChange.send()
@@ -4957,6 +4950,26 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
messages.append(systemMessage) messages.append(systemMessage)
} }
/// Add a system message to the mesh timeline only (never geohash).
/// If mesh is currently active, also append to the visible `messages`.
@MainActor
private func addMeshOnlySystemMessage(_ content: String) {
let systemMessage = BitchatMessage(
sender: "system",
content: content,
timestamp: Date(),
isRelay: false
)
// Persist to mesh timeline
meshTimeline.append(systemMessage)
trimMeshTimelineIfNeeded()
// Only show inline if mesh is the active channel
if case .mesh = activeChannel {
messages.append(systemMessage)
}
objectWillChange.send()
}
/// Public helper to add a system message to the public chat timeline. /// Public helper to add a system message to the public chat timeline.
/// Also persists the message into the active channel's backing store so it survives timeline rebinds. /// Also persists the message into the active channel's backing store so it survives timeline rebinds.
@MainActor @MainActor
@@ -5384,23 +5397,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session) SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
return return
} }
// Determine prior state to avoid duplicate system messages on repeated notifications
// Update the favorite relationship let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false
// Update the favorite relationship (idempotent storage)
FavoritesPersistenceService.shared.updatePeerFavoritedUs( FavoritesPersistenceService.shared.updatePeerFavoritedUs(
peerNoisePublicKey: finalNoiseKey, peerNoisePublicKey: finalNoiseKey,
favorited: isFavorite, favorited: isFavorite,
peerNickname: senderNickname, peerNickname: senderNickname,
peerNostrPublicKey: nostrPubkey peerNostrPublicKey: nostrPubkey
) )
// If they favorited us and provided their Nostr key, ensure it's stored // If they favorited us and provided their Nostr key, ensure it's stored (log only)
if isFavorite && nostrPubkey != nil { if isFavorite && nostrPubkey != nil {
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session) SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
} }
// Show system message // Only show a system message when the state changes, and only in mesh
let action = isFavorite ? "favorited" : "unfavorited" if prior != isFavorite {
addSystemMessage("\(senderNickname) \(action) you") let action = isFavorite ? "favorited" : "unfavorited"
addMeshOnlySystemMessage("\(senderNickname) \(action) you")
}
} }
@MainActor @MainActor