mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 11:05:19 +00:00
Inject notification/favorites singletons through coordinator contexts
NotificationService.shared and FavoritesPersistenceService.shared accesses across nine coordinators/components consolidate into ChatViewModel witnesses behind intent-named context members (notifyPrivateMessage, notifyMention, favoriteRelationship(forNoiseKey:), allFavoriteRelationships, postLocalNotification, ...). Singleton reach from coordinators is now zero; nine new tests cover previously untestable notification/favorites flows. Also root-causes the long-flaky gift-wrap dedup test: parallel tests share LocationChannelManager.shared, so channel-switch tests trigger clearProcessedNostrEvents() on every live ChatViewModel, wiping the dedup record mid-test. The invariant (forged-signature copies never poison dedup) now lives as a deterministic NostrInboundPipeline mock-context test; two Schnorr concurrency probes added along the way stay as regression guards for the P256K shared-context assumptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,10 @@ protocol ChatLifecycleContext: AnyObject {
|
||||
func deriveNostrIdentity(forGeohash geohash: String) throws -> NostrIdentity
|
||||
func recordGeoParticipant(pubkeyHex: String)
|
||||
|
||||
// MARK: Favorites (shared with `ChatPrivateConversationContext`)
|
||||
/// The persisted favorite relationship for the peer's Noise static key, if any.
|
||||
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship?
|
||||
|
||||
// MARK: Identity persistence
|
||||
/// Forces the identity manager to persist its state now.
|
||||
func forceSaveIdentity()
|
||||
@@ -69,7 +73,8 @@ extension ChatViewModel: ChatLifecycleContext {
|
||||
// `synchronizePrivateConversationStore()`, `addSystemMessage(_:)`,
|
||||
// `peerNickname(for:)`, `unifiedPeer(for:)`, `noiseSessionState(for:)`,
|
||||
// the routing/ack members, `isTeleported`,
|
||||
// `deriveNostrIdentity(forGeohash:)`, and `recordGeoParticipant(pubkeyHex:)`
|
||||
// `deriveNostrIdentity(forGeohash:)`, `recordGeoParticipant(pubkeyHex:)`,
|
||||
// and `favoriteRelationship(forNoiseKey:)`
|
||||
// are shared requirements with the other contexts or satisfied by
|
||||
// existing `ChatViewModel` members. The members below flatten nested
|
||||
// service accesses into intent-named calls.
|
||||
@@ -192,12 +197,12 @@ final class ChatLifecycleCoordinator {
|
||||
var peerNostrPubkey: String?
|
||||
|
||||
if let noiseKey = Data(hexString: peerID.id),
|
||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
|
||||
let favoriteStatus = context.favoriteRelationship(forNoiseKey: noiseKey) {
|
||||
noiseKeyHex = peerID
|
||||
peerNostrPubkey = favoriteStatus.peerNostrPublicKey
|
||||
} else if let peer = context.unifiedPeer(for: peerID) {
|
||||
noiseKeyHex = PeerID(hexData: peer.noisePublicKey)
|
||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey)
|
||||
let favoriteStatus = context.favoriteRelationship(forNoiseKey: peer.noisePublicKey)
|
||||
peerNostrPubkey = favoriteStatus?.peerNostrPublicKey
|
||||
|
||||
if let noiseKeyHex, context.unreadPrivateMessages.contains(noiseKeyHex) {
|
||||
|
||||
@@ -20,12 +20,23 @@ protocol ChatNostrContext: GeohashSubscriptionContext, NostrInboundPipelineConte
|
||||
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
|
||||
func sendGeohashDeliveryAck(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity)
|
||||
func sendGeohashReadReceipt(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity)
|
||||
|
||||
// MARK: Favorites & notifications (shared with the other contexts)
|
||||
/// The persisted favorite relationship for the peer's Noise static key, if any.
|
||||
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship?
|
||||
/// Adds (or updates) a favorite in the favorites store.
|
||||
func addFavorite(noiseKey: Data, nostrPublicKey: String?, nickname: String)
|
||||
/// Posts a generic local user notification.
|
||||
func postLocalNotification(title: String, body: String, identifier: String)
|
||||
}
|
||||
|
||||
extension ChatViewModel: ChatNostrContext {
|
||||
// All requirements — including the component-context witnesses declared
|
||||
// in `GeohashSubscriptionManager.swift`, `NostrInboundPipeline.swift`,
|
||||
// and `GeoPresenceTracker.swift` — already exist on `ChatViewModel`.
|
||||
// `GeoPresenceTracker.swift`, and the favorites/notification witnesses in
|
||||
// `ChatPrivateConversationCoordinator.swift`,
|
||||
// `ChatPeerIdentityCoordinator.swift`, and
|
||||
// `ChatVerificationCoordinator.swift` — already exist on `ChatViewModel`.
|
||||
}
|
||||
|
||||
/// Thin facade over the Nostr stack: owns and wires the three components and
|
||||
@@ -89,16 +100,17 @@ final class ChatNostrCoordinator {
|
||||
|
||||
@MainActor
|
||||
func handleFavoriteNotification(content: String, from nostrPubkey: String) {
|
||||
guard let context else { return }
|
||||
guard let senderNoiseKey = inbound.findNoiseKey(for: nostrPubkey) else { return }
|
||||
|
||||
let isFavorite = content.contains("FAVORITE:TRUE")
|
||||
let senderNickname = content.components(separatedBy: "|").last ?? "Unknown"
|
||||
|
||||
if isFavorite {
|
||||
FavoritesPersistenceService.shared.addFavorite(
|
||||
peerNoisePublicKey: senderNoiseKey,
|
||||
peerNostrPublicKey: nostrPubkey,
|
||||
peerNickname: senderNickname
|
||||
context.addFavorite(
|
||||
noiseKey: senderNoiseKey,
|
||||
nostrPublicKey: nostrPubkey,
|
||||
nickname: senderNickname
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,14 +135,14 @@ final class ChatNostrCoordinator {
|
||||
"💾 Storing Nostr key association for \(senderNickname): \(extractedNostrPubkey!.prefix(16))...",
|
||||
category: .session
|
||||
)
|
||||
FavoritesPersistenceService.shared.addFavorite(
|
||||
peerNoisePublicKey: senderNoiseKey,
|
||||
peerNostrPublicKey: extractedNostrPubkey,
|
||||
peerNickname: senderNickname
|
||||
context.addFavorite(
|
||||
noiseKey: senderNoiseKey,
|
||||
nostrPublicKey: extractedNostrPubkey,
|
||||
nickname: senderNickname
|
||||
)
|
||||
}
|
||||
|
||||
NotificationService.shared.sendLocalNotification(
|
||||
context.postLocalNotification(
|
||||
title: isFavorite ? "New Favorite" : "Favorite Removed",
|
||||
body: "\(senderNickname) \(isFavorite ? "favorited" : "unfavorited") you",
|
||||
identifier: "fav-\(UUID().uuidString)"
|
||||
@@ -140,7 +152,7 @@ final class ChatNostrCoordinator {
|
||||
@MainActor
|
||||
func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
|
||||
guard let context else { return }
|
||||
guard let relationship = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey),
|
||||
guard let relationship = context.favoriteRelationship(forNoiseKey: noisePublicKey),
|
||||
relationship.peerNostrPublicKey != nil else {
|
||||
SecureLogger.warning("⚠️ Cannot send favorite notification - no Nostr key for peer", category: .session)
|
||||
return
|
||||
|
||||
@@ -76,6 +76,16 @@ protocol ChatPeerIdentityContext: AnyObject {
|
||||
func invalidateStoredEncryptionCache(for peerID: PeerID?)
|
||||
func socialIdentity(forFingerprint fingerprint: String) -> SocialIdentity?
|
||||
|
||||
// MARK: Favorites
|
||||
/// The persisted favorite relationship for the peer's Noise static key, if any.
|
||||
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship?
|
||||
/// The persisted favorite relationship for a short (ephemeral) peer ID, if any.
|
||||
func favoriteRelationship(forPeerID peerID: PeerID) -> FavoritesPersistenceService.FavoriteRelationship?
|
||||
/// Adds (or updates) a favorite in the favorites store.
|
||||
func addFavorite(noiseKey: Data, nostrPublicKey: String?, nickname: String)
|
||||
/// Removes a favorite from the favorites store.
|
||||
func removeFavorite(noiseKey: Data)
|
||||
|
||||
// MARK: Geohash & Nostr
|
||||
var geoNicknames: [String: String] { get }
|
||||
func visibleGeohashPeople() -> [GeoPerson]
|
||||
@@ -183,6 +193,26 @@ extension ChatViewModel: ChatPeerIdentityContext {
|
||||
func bridgedNostrPublicKey(for noiseKey: Data) -> String? {
|
||||
idBridge.getNostrPublicKey(for: noiseKey)
|
||||
}
|
||||
|
||||
// `favoriteRelationship(forNoiseKey:)` is shared with
|
||||
// `ChatPrivateConversationContext`; its witness lives in
|
||||
// `ChatPrivateConversationCoordinator.swift`.
|
||||
|
||||
func favoriteRelationship(forPeerID peerID: PeerID) -> FavoritesPersistenceService.FavoriteRelationship? {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID)
|
||||
}
|
||||
|
||||
func addFavorite(noiseKey: Data, nostrPublicKey: String?, nickname: String) {
|
||||
FavoritesPersistenceService.shared.addFavorite(
|
||||
peerNoisePublicKey: noiseKey,
|
||||
peerNostrPublicKey: nostrPublicKey,
|
||||
peerNickname: nickname
|
||||
)
|
||||
}
|
||||
|
||||
func removeFavorite(noiseKey: Data) {
|
||||
FavoritesPersistenceService.shared.removeFavorite(peerNoisePublicKey: noiseKey)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatPeerIdentityCoordinator {
|
||||
@@ -255,7 +285,7 @@ final class ChatPeerIdentityCoordinator {
|
||||
@MainActor
|
||||
func isFavorite(peerID: PeerID) -> Bool {
|
||||
if let noisePublicKey = peerID.noiseKey {
|
||||
return FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey)?.isFavorite ?? false
|
||||
return context.favoriteRelationship(forNoiseKey: noisePublicKey)?.isFavorite ?? false
|
||||
}
|
||||
|
||||
return context.unifiedPeer(for: peerID)?.isFavorite ?? false
|
||||
@@ -499,12 +529,12 @@ final class ChatPeerIdentityCoordinator {
|
||||
if let name = context.peerNickname(for: peerID) {
|
||||
return name
|
||||
}
|
||||
if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
||||
if let favorite = context.favoriteRelationship(forPeerID: peerID),
|
||||
!favorite.peerNickname.isEmpty {
|
||||
return favorite.peerNickname
|
||||
}
|
||||
if let noiseKey = Data(hexString: peerID.id),
|
||||
let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||
let favorite = context.favoriteRelationship(forNoiseKey: noiseKey),
|
||||
!favorite.peerNickname.isEmpty {
|
||||
return favorite.peerNickname
|
||||
}
|
||||
@@ -579,7 +609,7 @@ private extension ChatPeerIdentityCoordinator {
|
||||
if let nickname = context.peerNickname(for: peerID) {
|
||||
return nickname
|
||||
}
|
||||
if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: peerPublicKey) {
|
||||
if let favorite = context.favoriteRelationship(forNoiseKey: peerPublicKey) {
|
||||
return favorite.peerNickname
|
||||
}
|
||||
return "Unknown"
|
||||
@@ -602,7 +632,7 @@ private extension ChatPeerIdentityCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
let currentStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey)
|
||||
let currentStatus = context.favoriteRelationship(forNoiseKey: noisePublicKey)
|
||||
let fallbackNickname = context.privateChats[peerID]?.first { $0.senderPeerID == peerID }?.sender
|
||||
let plan = ChatFavoriteTogglePolicy.plan(
|
||||
currentStatus: currentStatus.map(ChatFavoriteStatusSnapshot.init),
|
||||
@@ -612,14 +642,14 @@ private extension ChatPeerIdentityCoordinator {
|
||||
|
||||
switch plan.persistenceAction {
|
||||
case .add(let nickname, let nostrKey):
|
||||
FavoritesPersistenceService.shared.addFavorite(
|
||||
peerNoisePublicKey: noisePublicKey,
|
||||
peerNostrPublicKey: nostrKey,
|
||||
peerNickname: nickname
|
||||
context.addFavorite(
|
||||
noiseKey: noisePublicKey,
|
||||
nostrPublicKey: nostrKey,
|
||||
nickname: nickname
|
||||
)
|
||||
|
||||
case .remove:
|
||||
FavoritesPersistenceService.shared.removeFavorite(peerNoisePublicKey: noisePublicKey)
|
||||
context.removeFavorite(noiseKey: noisePublicKey)
|
||||
}
|
||||
|
||||
context.notifyUIChanged()
|
||||
|
||||
@@ -28,6 +28,10 @@ protocol ChatPeerListContext: AnyObject {
|
||||
func activeMeshPeerCount() -> Int
|
||||
func registerEphemeralSession(peerID: PeerID)
|
||||
func updateEncryptionStatusForPeers()
|
||||
|
||||
// MARK: Notifications
|
||||
/// Posts the "bitchatters nearby" local notification.
|
||||
func notifyNetworkAvailable(peerCount: Int)
|
||||
}
|
||||
|
||||
extension ChatViewModel: ChatPeerListContext {
|
||||
@@ -48,6 +52,10 @@ extension ChatViewModel: ChatPeerListContext {
|
||||
}
|
||||
.count
|
||||
}
|
||||
|
||||
func notifyNetworkAvailable(peerCount: Int) {
|
||||
NotificationService.shared.sendNetworkAvailableNotification(peerCount: peerCount)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatPeerListCoordinator: @unchecked Sendable {
|
||||
@@ -115,7 +123,7 @@ private extension ChatPeerListCoordinator {
|
||||
if Date().timeIntervalSince(lastNetworkNotificationTime) >= cooldown {
|
||||
recentlySeenPeers.formUnion(newPeers)
|
||||
lastNetworkNotificationTime = Date()
|
||||
NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count)
|
||||
context.notifyNetworkAvailable(peerCount: meshPeers.count)
|
||||
SecureLogger.info(
|
||||
"👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers (new: \(newPeers.count))",
|
||||
category: .session
|
||||
|
||||
@@ -69,6 +69,14 @@ protocol ChatPrivateConversationContext: AnyObject {
|
||||
func addSystemMessage(_ content: String)
|
||||
func addMeshOnlySystemMessage(_ content: String)
|
||||
func sanitizeChat(for peerID: PeerID)
|
||||
|
||||
// MARK: Favorites & notifications
|
||||
/// The persisted favorite relationship for the peer's Noise static key, if any.
|
||||
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship?
|
||||
/// Persists that the peer favorited/unfavorited us (favorites store write).
|
||||
func updatePeerFavoritedUs(noiseKey: Data, favorited: Bool, nickname: String, nostrPublicKey: String?)
|
||||
/// Posts the incoming-private-message local notification.
|
||||
func notifyPrivateMessage(from senderName: String, message: String, peerID: PeerID)
|
||||
}
|
||||
|
||||
extension ChatViewModel: ChatPrivateConversationContext {
|
||||
@@ -161,6 +169,23 @@ extension ChatViewModel: ChatPrivateConversationContext {
|
||||
privateChatManager.sanitizeChat(for: peerID)
|
||||
}
|
||||
|
||||
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
||||
}
|
||||
|
||||
func updatePeerFavoritedUs(noiseKey: Data, favorited: Bool, nickname: String, nostrPublicKey: String?) {
|
||||
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
||||
peerNoisePublicKey: noiseKey,
|
||||
favorited: favorited,
|
||||
peerNickname: nickname,
|
||||
peerNostrPublicKey: nostrPublicKey
|
||||
)
|
||||
}
|
||||
|
||||
func notifyPrivateMessage(from senderName: String, message: String, peerID: PeerID) {
|
||||
NotificationService.shared.sendPrivateMessageNotification(from: senderName, message: message, peerID: peerID)
|
||||
}
|
||||
|
||||
private func makeGeohashNostrTransport() -> NostrTransport {
|
||||
let transport = NostrTransport(keychain: keychain, idBridge: idBridge)
|
||||
transport.senderPeerID = meshService.myPeerID
|
||||
@@ -199,7 +224,7 @@ final class ChatPrivateConversationCoordinator {
|
||||
guard let noiseKey = Data(hexString: peerID.id) else { return }
|
||||
let isConnected = context.isPeerConnected(peerID)
|
||||
let isReachable = context.isPeerReachable(peerID)
|
||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
||||
let favoriteStatus = context.favoriteRelationship(forNoiseKey: noiseKey)
|
||||
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
||||
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
||||
|
||||
@@ -393,11 +418,7 @@ final class ChatPrivateConversationCoordinator {
|
||||
}
|
||||
|
||||
if !isViewing && shouldMarkUnread {
|
||||
NotificationService.shared.sendPrivateMessageNotification(
|
||||
from: senderName,
|
||||
message: pm.content,
|
||||
peerID: convKey
|
||||
)
|
||||
context.notifyPrivateMessage(from: senderName, message: pm.content, peerID: convKey)
|
||||
}
|
||||
|
||||
context.notifyUIChanged()
|
||||
@@ -592,11 +613,7 @@ final class ChatPrivateConversationCoordinator {
|
||||
context.markReadReceiptSent(message.id)
|
||||
} else {
|
||||
context.unreadPrivateMessages.insert(peerID)
|
||||
NotificationService.shared.sendPrivateMessageNotification(
|
||||
from: message.sender,
|
||||
message: message.content,
|
||||
peerID: peerID
|
||||
)
|
||||
context.notifyPrivateMessage(from: message.sender, message: message.content, peerID: peerID)
|
||||
}
|
||||
|
||||
context.notifyUIChanged()
|
||||
@@ -692,11 +709,7 @@ final class ChatPrivateConversationCoordinator {
|
||||
context.unreadPrivateMessages.insert(ephemeralPeerID)
|
||||
}
|
||||
if isRecentMessage {
|
||||
NotificationService.shared.sendPrivateMessageNotification(
|
||||
from: senderNickname,
|
||||
message: messageContent,
|
||||
peerID: targetPeerID
|
||||
)
|
||||
context.notifyPrivateMessage(from: senderNickname, message: messageContent, peerID: targetPeerID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,12 +729,12 @@ final class ChatPrivateConversationCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false
|
||||
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
||||
peerNoisePublicKey: finalNoiseKey,
|
||||
let prior = context.favoriteRelationship(forNoiseKey: finalNoiseKey)?.theyFavoritedUs ?? false
|
||||
context.updatePeerFavoritedUs(
|
||||
noiseKey: finalNoiseKey,
|
||||
favorited: isFavorite,
|
||||
peerNickname: senderNickname,
|
||||
peerNostrPublicKey: nostrPubkey
|
||||
nickname: senderNickname,
|
||||
nostrPublicKey: nostrPubkey
|
||||
)
|
||||
|
||||
if isFavorite && nostrPubkey != nil {
|
||||
|
||||
@@ -88,6 +88,10 @@ protocol ChatPublicConversationContext: AnyObject {
|
||||
func recordContentKey(_ key: String, timestamp: Date)
|
||||
/// Pre-renders the message so the formatting cache is warm before display.
|
||||
func prewarmMessageFormatting(_ message: BitchatMessage)
|
||||
|
||||
// MARK: Notifications
|
||||
/// Posts the you-were-mentioned local notification.
|
||||
func notifyMention(from sender: String, message: String)
|
||||
}
|
||||
|
||||
extension ChatViewModel: ChatPublicConversationContext {
|
||||
@@ -184,6 +188,10 @@ extension ChatViewModel: ChatPublicConversationContext {
|
||||
func prewarmMessageFormatting(_ message: BitchatMessage) {
|
||||
_ = formatMessageAsText(message, colorScheme: currentColorScheme)
|
||||
}
|
||||
|
||||
func notifyMention(from sender: String, message: String) {
|
||||
NotificationService.shared.sendMentionNotification(from: sender, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -548,7 +556,7 @@ final class ChatPublicConversationCoordinator: PublicMessagePipelineDelegate {
|
||||
|
||||
if isMentioned && message.sender != context.nickname {
|
||||
SecureLogger.info("🔔 Mention from \(message.sender)", category: .session)
|
||||
NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content)
|
||||
context.notifyMention(from: message.sender, message: message.content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ protocol ChatVerificationContext: AnyObject {
|
||||
func triggerHandshake(with peerID: PeerID)
|
||||
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||
|
||||
// MARK: Notifications (shared with `ChatNostrContext`)
|
||||
/// Posts a generic local user notification.
|
||||
func postLocalNotification(title: String, body: String, identifier: String)
|
||||
}
|
||||
|
||||
extension ChatViewModel: ChatVerificationContext {
|
||||
@@ -110,6 +114,10 @@ extension ChatViewModel: ChatVerificationContext {
|
||||
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data) {
|
||||
meshService.sendVerifyResponse(to: peerID, noiseKeyHex: noiseKeyHex, nonceA: nonceA)
|
||||
}
|
||||
|
||||
func postLocalNotification(title: String, body: String, identifier: String) {
|
||||
NotificationService.shared.sendLocalNotification(title: title, body: body, identifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -313,7 +321,7 @@ final class ChatVerificationCoordinator {
|
||||
|
||||
let peerName = context.unifiedPeer(for: peerID)?.nickname
|
||||
?? context.resolveNickname(for: peerID)
|
||||
NotificationService.shared.sendLocalNotification(
|
||||
context.postLocalNotification(
|
||||
title: "Verified",
|
||||
body: "You verified \(peerName)",
|
||||
identifier: "verify-success-\(peerID)-\(UUID().uuidString)"
|
||||
@@ -347,7 +355,7 @@ private extension ChatVerificationCoordinator {
|
||||
guard now.timeIntervalSince(lastToast) > 60 else { return }
|
||||
|
||||
lastMutualToastAt[fingerprint] = now
|
||||
NotificationService.shared.sendLocalNotification(
|
||||
context.postLocalNotification(
|
||||
title: title,
|
||||
body: "You and \(bodyName) verified each other",
|
||||
identifier: "\(notificationPrefix)-\(peerID)-\(UUID().uuidString)"
|
||||
|
||||
@@ -25,6 +25,9 @@ protocol GeoPresenceContext: AnyObject {
|
||||
|
||||
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool
|
||||
func synchronizePublicConversationStore(forGeohash geohash: String)
|
||||
|
||||
/// Posts the sampled-geohash-activity local notification.
|
||||
func notifyGeohashActivity(geohash: String, bodyPreview: String)
|
||||
}
|
||||
|
||||
extension ChatViewModel: GeoPresenceContext {
|
||||
@@ -52,6 +55,10 @@ extension ChatViewModel: GeoPresenceContext {
|
||||
func markGeoTeleported(_ pubkeyHexLowercased: String) {
|
||||
locationPresenceStore.markTeleported(pubkeyHexLowercased)
|
||||
}
|
||||
|
||||
func notifyGeohashActivity(geohash: String, bodyPreview: String) {
|
||||
NotificationService.shared.sendGeohashActivityNotification(geohash: geohash, bodyPreview: bodyPreview)
|
||||
}
|
||||
}
|
||||
|
||||
/// Geohash presence bookkeeping that is independent of relay subscriptions:
|
||||
@@ -160,7 +167,7 @@ final class GeoPresenceTracker {
|
||||
)
|
||||
if context.appendGeohashMessageIfAbsent(message, toGeohash: gh) {
|
||||
context.synchronizePublicConversationStore(forGeohash: gh)
|
||||
NotificationService.shared.sendGeohashActivityNotification(geohash: gh, bodyPreview: preview)
|
||||
context.notifyGeohashActivity(geohash: gh, bodyPreview: preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ protocol NostrInboundPipelineContext: AnyObject {
|
||||
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool
|
||||
func displayNameForNostrPubkey(_ pubkeyHex: String) -> String
|
||||
|
||||
// MARK: Favorites bridge
|
||||
/// All favorite relationships, used to bridge a Nostr pubkey back to a
|
||||
/// Noise key on the inbound DM path.
|
||||
func allFavoriteRelationships() -> [FavoritesPersistenceService.FavoriteRelationship]
|
||||
|
||||
// MARK: Presence & key mapping
|
||||
func setGeoNickname(_ nickname: String, forPubkey pubkeyHex: String)
|
||||
/// Records the Nostr pubkey behind a (possibly virtual) peer ID.
|
||||
@@ -53,6 +58,10 @@ extension ChatViewModel: NostrInboundPipelineContext {
|
||||
deduplicationService.hasProcessedNostrEvent(eventID)
|
||||
}
|
||||
|
||||
func allFavoriteRelationships() -> [FavoritesPersistenceService.FavoriteRelationship] {
|
||||
Array(FavoritesPersistenceService.shared.favorites.values)
|
||||
}
|
||||
|
||||
func recordProcessedNostrEvent(_ eventID: String) {
|
||||
deduplicationService.recordNostrEvent(eventID)
|
||||
}
|
||||
@@ -437,7 +446,8 @@ final class NostrInboundPipeline {
|
||||
/// the favorites glue in `ChatNostrCoordinator` delegates to it.
|
||||
@MainActor
|
||||
func findNoiseKey(for nostrPubkey: String) -> Data? {
|
||||
let favorites = FavoritesPersistenceService.shared.favorites.values
|
||||
guard let context else { return nil }
|
||||
let favorites = context.allFavoriteRelationships()
|
||||
var npubToMatch = nostrPubkey
|
||||
|
||||
if !nostrPubkey.hasPrefix("npub") {
|
||||
|
||||
Reference in New Issue
Block a user