bitchat/bitchat/ViewModels/ChatUnreadStateResolver.swift
Taksh bc20ed6add feat: clear unread badge for a single conversation
Adds a mark-as-read action to the mesh, groups and chats rows in the people
sheet, and to the header of an open DM. Clearing the badge sends no read
receipt — the unread state is local, so dismissing it should stay local.

Badge clearing is alias-aware: ChatUnreadStateResolver.matchingUnreadPeerIDs
collects the noise-key, nostr and geoDM-nickname peer IDs that resolve to the
same conversation, mirroring hasUnreadMessages. Clearing only the routing peer
ID left ghost badges behind on the aliases.

Rebased onto current main. The catalog carries only the three new keys,
spliced in Xcode's serialization; main's 520 existing keys are untouched.
The chats section main gained in #1598 gets the same action.
2026-08-12 05:25:38 +05:30

79 lines
2.4 KiB
Swift

import BitFoundation
import Foundation
struct ChatUnreadPeerContext {
let peerID: PeerID
let noiseKeyPeerID: PeerID?
let nostrPeerID: PeerID?
let nickname: String?
}
enum ChatUnreadStateResolver {
static func hasUnreadMessages(
for context: ChatUnreadPeerContext,
unreadPrivateMessages: Set<PeerID>,
privateChats: [PeerID: [BitchatMessage]]
) -> Bool {
if unreadPrivateMessages.contains(context.peerID) {
return true
}
if let noiseKeyPeerID = context.noiseKeyPeerID,
unreadPrivateMessages.contains(noiseKeyPeerID) {
return true
}
if let nostrPeerID = context.nostrPeerID,
unreadPrivateMessages.contains(nostrPeerID) {
return true
}
guard let peerNickname = context.nickname?.lowercased(), !peerNickname.isEmpty else {
return false
}
return unreadPrivateMessages.contains { unreadPeerID in
guard unreadPeerID.isGeoDM,
let firstMessage = privateChats[unreadPeerID]?.first else {
return false
}
return firstMessage.sender.lowercased() == peerNickname
}
}
/// Peer IDs in `unreadPrivateMessages` that resolve to the same
/// conversation badge as `hasUnreadMessages(for:)`.
static func matchingUnreadPeerIDs(
for context: ChatUnreadPeerContext,
unreadPrivateMessages: Set<PeerID>,
privateChats: [PeerID: [BitchatMessage]]
) -> Set<PeerID> {
var matching = Set<PeerID>()
if unreadPrivateMessages.contains(context.peerID) {
matching.insert(context.peerID)
}
if let noiseKeyPeerID = context.noiseKeyPeerID,
unreadPrivateMessages.contains(noiseKeyPeerID) {
matching.insert(noiseKeyPeerID)
}
if let nostrPeerID = context.nostrPeerID,
unreadPrivateMessages.contains(nostrPeerID) {
matching.insert(nostrPeerID)
}
if let peerNickname = context.nickname?.lowercased(), !peerNickname.isEmpty {
for unreadPeerID in unreadPrivateMessages where unreadPeerID.isGeoDM {
guard let firstMessage = privateChats[unreadPeerID]?.first else { continue }
if firstMessage.sender.lowercased() == peerNickname {
matching.insert(unreadPeerID)
}
}
}
return matching
}
}