From e3b22f35983578948266ebffd8c32651eb1e52c2 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 9 Aug 2026 11:48:24 +0200 Subject: [PATCH] Review fixes: gate nostr availability on a stored recipient key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Codex P2 / review: a mutual favorite with a nil peerNostrPublicKey still reported .nostrAvailable, so the DM caption could claim "end-to-end encrypted" with neither a Noise session nor a usable Nostr key. BitchatPeer.connectionState (and the header fallback in PrivateConversationModel) now require a stored recipient key — mirroring NostrTransport's reachability rule. Regression test: mutual + nil key → .offline; key present → .nostrAvailable. - Copy: "local-only identity" → "device-local identity" (all 30 locales) — once a fingerprint or QR is shared it isn't "only" local; device-local says where the keys live without over-claiming. Co-Authored-By: Claude Fable 5 --- bitchat/App/PrivateConversationModels.swift | 5 +- bitchat/Localizable.xcstrings | 60 ++++++++++----------- bitchat/Models/BitchatPeer.swift | 13 ++++- bitchatTests/BitchatPeerTests.swift | 24 +++++++++ 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/bitchat/App/PrivateConversationModels.swift b/bitchat/App/PrivateConversationModels.swift index a4e89149..80d5cf29 100644 --- a/bitchat/App/PrivateConversationModels.swift +++ b/bitchat/App/PrivateConversationModels.swift @@ -357,7 +357,10 @@ final class PrivateConversationModel: ObservableObject { } if let noiseKey = Data(hexString: headerPeerID.id), let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), - favoriteStatus.isMutual { + favoriteStatus.isMutual, + // No stored recipient key means no Nostr delivery — and the + // "end-to-end encrypted" caption keys off .nostrAvailable. + favoriteStatus.peerNostrPublicKey != nil { return .nostrAvailable } if chatViewModel.meshService.isPeerConnected(headerPeerID) || chatViewModel.connectedPeers.contains(headerPeerID) { diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index 219e7bf7..df20c377 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -11560,181 +11560,181 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "هوية محلية فقط" + "value" : "هوية محفوظة على الجهاز" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "শুধুই স্থানীয় পরিচয়" + "value" : "ডিভাইসে থাকা পরিচয়" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "nur lokale identität" + "value" : "gerätelokale identität" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "local-only identity" + "value" : "device-local identity" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "identidad solo local" + "value" : "identidad local del dispositivo" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "هویت فقط محلی" + "value" : "هویت محلی دستگاه" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "pagkakakilanlang lokal lamang" + "value" : "pagkakakilanlang nasa device" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "identité locale uniquement" + "value" : "identité locale à l'appareil" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "זהות מקומית בלבד" + "value" : "זהות מקומית במכשיר" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "केवल-स्थानीय पहचान" + "value" : "डिवाइस-स्थानीय पहचान" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "identitas hanya lokal" + "value" : "identitas lokal perangkat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "identità solo locale" + "value" : "identità locale al dispositivo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ローカル限定のアイデンティティ" + "value" : "端末ローカルのアイデンティティ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "이 기기에만 있는 신원" + "value" : "기기 로컬 신원" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "identiti setempat sahaja" + "value" : "identiti setempat peranti" } }, "ne" : { "stringUnit" : { "state" : "translated", - "value" : "स्थानीय-मात्र परिचय" + "value" : "यन्त्र-स्थानीय परिचय" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "alleen-lokale identiteit" + "value" : "apparaat-lokale identiteit" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "tożsamość tylko lokalna" + "value" : "tożsamość lokalna urządzenia" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "identidade apenas local" + "value" : "identidade local do dispositivo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "identidade apenas local" + "value" : "identidade local do dispositivo" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "идентичность только на устройстве" + "value" : "идентичность на устройстве" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "endast lokal identitet" + "value" : "enhetslokal identitet" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உள்ளூர்-மட்டும் அடையாளம்" + "value" : "சாதன-உள்ளூர் அடையாளம்" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ตัวตนเฉพาะในเครื่อง" + "value" : "ตัวตนในเครื่อง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "yalnızca yerel kimlik" + "value" : "cihaza yerel kimlik" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ідентичність лише на пристрої" + "value" : "ідентичність на пристрої" } }, "ur" : { "stringUnit" : { "state" : "translated", - "value" : "صرف مقامی شناخت" + "value" : "ڈیوائس مقامی شناخت" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "danh tính chỉ trên thiết bị" + "value" : "danh tính cục bộ trên thiết bị" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "仅限本机的身份" + "value" : "设备本地身份" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "僅限本機的身分" + "value" : "裝置本機身分" } } } diff --git a/bitchat/Models/BitchatPeer.swift b/bitchat/Models/BitchatPeer.swift index 61c0a5f8..afa7c53d 100644 --- a/bitchat/Models/BitchatPeer.swift +++ b/bitchat/Models/BitchatPeer.swift @@ -33,13 +33,22 @@ struct BitchatPeer: Equatable { return .bluetoothConnected } else if isReachable { return .meshReachable - } else if favoriteStatus?.isMutual == true { - // Mutual favorites can communicate via Nostr when offline + } else if favoriteStatus?.isMutual == true, reachableNostrPublicKey != nil { + // Mutual favorites can communicate via Nostr when offline — but + // only with a stored recipient key. NostrTransport applies the + // same rule when computing reachable peers; without a key, + // "available" would be a lie, and the DM header's + // "end-to-end encrypted" caption keys off this state. return .nostrAvailable } else { return .offline } } + + /// The Nostr key a private envelope would actually be sealed to, if any. + var reachableNostrPublicKey: String? { + nostrPublicKey ?? favoriteStatus?.peerNostrPublicKey + } var isFavorite: Bool { favoriteStatus?.isFavorite ?? false diff --git a/bitchatTests/BitchatPeerTests.swift b/bitchatTests/BitchatPeerTests.swift index eba4e4c7..101f30f8 100644 --- a/bitchatTests/BitchatPeerTests.swift +++ b/bitchatTests/BitchatPeerTests.swift @@ -36,6 +36,30 @@ struct BitchatPeerTests { #expect(peer.statusIcon == "🌙") } + @Test("Mutual favorite without a stored Nostr key is offline, not nostr-available") + func mutualFavoriteWithoutNostrKeyIsOffline() { + let peerID = PeerID(str: "0123456789abcdef") + let noiseKey = Data((0..<32).map(UInt8.init)) + var peer = BitchatPeer(peerID: peerID, noisePublicKey: noiseKey, nickname: "A", isConnected: false, isReachable: false) + peer.favoriteStatus = FavoriteRelationship( + peerNoisePublicKey: noiseKey, + peerNostrPublicKey: nil, + peerNickname: "A", + isFavorite: true, + theyFavoritedUs: true, + favoritedAt: Date(timeIntervalSince1970: 1), + lastUpdated: Date(timeIntervalSince1970: 2) + ) + + // Nothing to seal a Nostr envelope to: claiming availability here + // would relight the DM header's "end-to-end encrypted" caption with + // neither a Noise session nor a usable recipient key. + #expect(peer.connectionState == .offline) + + peer.nostrPublicKey = "abcdef" + #expect(peer.connectionState == .nostrAvailable) + } + @Test("Mutual offline peers show Nostr icon") func mutualFavoriteOfflinePeerShowsNostrIcon() { let peerID = PeerID(str: "0011223344556677")