From bb3d99bdca1237f8cdf3e13835e85f8d658c01e5 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:36:32 +0200 Subject: [PATCH] Fix repeated favorite notifications; route system messages to mesh; simplify favorites (#588) * Favorites: mesh-only system message; stop reconnect resends; gate system on state change * Favorites: remove npub resend tracking and nickname-based key migration; rely on Noise key as identity --------- Co-authored-by: jack --- .../FavoritesPersistenceService.swift | 4 ++ bitchat/Services/MessageRouter.swift | 1 + bitchat/Services/UnifiedPeerService.swift | 21 -------- bitchat/ViewModels/ChatViewModel.swift | 53 ++++++++++++------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/bitchat/Services/FavoritesPersistenceService.swift b/bitchat/Services/FavoritesPersistenceService.swift index 82c3ffb8..7e07c10d 100644 --- a/bitchat/Services/FavoritesPersistenceService.swift +++ b/bitchat/Services/FavoritesPersistenceService.swift @@ -13,12 +13,16 @@ final class FavoritesPersistenceService: ObservableObject { let theyFavoritedUs: Bool let favoritedAt: 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 { 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 keychainService = "chat.bitchat.favorites" diff --git a/bitchat/Services/MessageRouter.swift b/bitchat/Services/MessageRouter.swift index 2ea05140..e4e4bb8a 100644 --- a/bitchat/Services/MessageRouter.swift +++ b/bitchat/Services/MessageRouter.swift @@ -74,6 +74,7 @@ final class MessageRouter { } func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { + // Route via mesh when connected; else use Nostr if mesh.isPeerConnected(peerID) { mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) } else { diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index cdef5858..30e91056 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -195,27 +195,6 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { let favoriteStatus = favorites[noiseKey] { peer.favoriteStatus = favoriteStatus 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 diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 05872bfc..8605744d 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -420,6 +420,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private var geoSamplingSubs: [String: String] = [:] // subID -> geohash private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time + // MARK: - Message Delivery Tracking // Delivery tracking @@ -4599,16 +4600,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Register ephemeral session with identity manager SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID) - // Check if we favorite this peer and resend notification on reconnect - // This ensures Nostr key mapping is maintained across reconnections - 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) - } + // Intentionally do not resend favorites on reconnect. + // We only send our npub when a favorite is toggled on, or if our npub changes. // Force UI refresh objectWillChange.send() @@ -4957,6 +4950,26 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { 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. /// Also persists the message into the active channel's backing store so it survives timeline rebinds. @MainActor @@ -5384,23 +5397,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session) return } - - // Update the favorite relationship + // Determine prior state to avoid duplicate system messages on repeated notifications + let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false + + // Update the favorite relationship (idempotent storage) FavoritesPersistenceService.shared.updatePeerFavoritedUs( peerNoisePublicKey: finalNoiseKey, favorited: isFavorite, peerNickname: senderNickname, 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 { SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session) } - - // Show system message - let action = isFavorite ? "favorited" : "unfavorited" - addSystemMessage("\(senderNickname) \(action) you") + + // Only show a system message when the state changes, and only in mesh + if prior != isFavorite { + let action = isFavorite ? "favorited" : "unfavorited" + addMeshOnlySystemMessage("\(senderNickname) \(action) you") + } } @MainActor