mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
Review fixes: unread flag + favorites-aware naming for the warning
- Codex P1: a background identity change appended the warning without marking the chat unread — a security event nobody was looking at stayed invisible until the conversation was manually opened. It now sets the unread flag unless the chat is open. - Codex P2: offline key rotation is the common case here, and resolveNickname falls back to an anon prefix precisely then (no mesh nickname, no social identity for the unverified new fingerprint). The persisted favorite relationship's nickname is preferred. Both pinned by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0a288869d6
commit
c58ad9af10
@ -24,6 +24,9 @@ protocol ChatPeerIdentityContext: AnyObject {
|
||||
var unreadPrivateMessages: Set<PeerID> { get }
|
||||
/// Clears the peer's unread flag (single-writer store intent).
|
||||
func markPrivateChatRead(_ peerID: PeerID)
|
||||
/// Sets the peer's unread flag (shared requirement with the inbound DM
|
||||
/// paths; witness on `ChatViewModel`).
|
||||
func markPrivateChatUnread(_ peerID: PeerID)
|
||||
/// Moves all messages from `oldPeerID`'s chat into `newPeerID`'s chat
|
||||
/// (dedup by ID, order preserved, unread carried, old chat removed).
|
||||
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
|
||||
@ -621,12 +624,20 @@ extension ChatPeerIdentityCoordinator {
|
||||
// inherit the thread's earned trust under the same nickname. Only
|
||||
// warn when there is a conversation to protect.
|
||||
if wasSelected || !context.privateMessages(for: newPeerID).isEmpty {
|
||||
// Offline key rotation is the common case here (a favorite came
|
||||
// back with new keys), and resolveNickname has no mesh nickname
|
||||
// and no social identity for a fingerprint nobody verified yet —
|
||||
// the persisted favorite relationship still knows who this is.
|
||||
let favoriteNickname = newPeerID.noiseKey
|
||||
.flatMap { context.favoriteRelationship(forNoiseKey: $0)?.peerNickname }
|
||||
.flatMap { $0.isEmpty ? nil : $0 }
|
||||
let displayName = favoriteNickname ?? resolveNickname(for: newPeerID)
|
||||
let notice = BitchatMessage(
|
||||
sender: "system",
|
||||
content: String(
|
||||
format: String(localized: "system.identity.key_changed", defaultValue: "%@'s identity key changed — this can mean a new device or a reset. earlier verification no longer applies; verify them again before trusting this chat.", comment: "Private-chat system warning after a peer's Noise identity key changed; placeholder is the peer's name"),
|
||||
locale: .current,
|
||||
resolveNickname(for: newPeerID)
|
||||
displayName
|
||||
),
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
@ -636,6 +647,12 @@ extension ChatPeerIdentityCoordinator {
|
||||
senderPeerID: context.myPeerID
|
||||
)
|
||||
context.appendPrivateMessage(notice, to: newPeerID)
|
||||
// A security event nobody is looking at must not stay silent:
|
||||
// surface it through the unread indicator unless the chat is
|
||||
// open right now.
|
||||
if !wasSelected {
|
||||
context.markPrivateChatUnread(newPeerID)
|
||||
}
|
||||
context.notifyUIChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,10 @@ private final class MockChatPeerIdentityContext: ChatPeerIdentityContext {
|
||||
unreadPrivateMessages.remove(peerID)
|
||||
}
|
||||
|
||||
func markPrivateChatUnread(_ peerID: PeerID) {
|
||||
unreadPrivateMessages.insert(peerID)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func appendPrivateMessage(_ message: BitchatMessage, to peerID: PeerID) -> Bool {
|
||||
var chat = privateChats[peerID] ?? []
|
||||
@ -390,6 +394,35 @@ struct ChatPeerIdentityCoordinatorContextTests {
|
||||
#expect(notice?.content.contains("identity key changed") == true)
|
||||
#expect(notice?.content.contains("alice") == true)
|
||||
#expect(context.privateChats[oldPeerID] == nil)
|
||||
// A background security event must surface via the unread indicator.
|
||||
#expect(context.unreadPrivateMessages.contains(newPeerID))
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func migrateNoiseKeyUpdate_namesOfflineFavoritesFromTheRelationship() async {
|
||||
let context = MockChatPeerIdentityContext()
|
||||
let coordinator = ChatPeerIdentityCoordinator(context: context)
|
||||
let oldPeerID = PeerID(str: "5555555555555555")
|
||||
let newKey = Data(repeating: 0xCD, count: 32)
|
||||
let newPeerID = PeerID(hexData: newKey)
|
||||
// Offline key rotation: no mesh nickname, no social identity for the
|
||||
// unverified new fingerprint — only the favorite relationship knows
|
||||
// who this is.
|
||||
context.favoriteRelationshipsByNoiseKey[newKey] = FavoritesPersistenceService.FavoriteRelationship(
|
||||
peerNoisePublicKey: newKey,
|
||||
peerNostrPublicKey: nil,
|
||||
peerNickname: "carol",
|
||||
isFavorite: true,
|
||||
theyFavoritedUs: true,
|
||||
favoritedAt: Date(timeIntervalSince1970: 0),
|
||||
lastUpdated: Date(timeIntervalSince1970: 0)
|
||||
)
|
||||
context.privateChats[oldPeerID] = [makePrivateMessage(id: "m2", timestamp: Date(timeIntervalSince1970: 1))]
|
||||
|
||||
coordinator.migrateNoiseKeyUpdate(oldPeerID: oldPeerID, newPeerID: newPeerID)
|
||||
|
||||
let notice = (context.privateChats[newPeerID] ?? []).last
|
||||
#expect(notice?.content.contains("carol") == true)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user