From fb60c929eb1794d0faab9da45be6ee7f8a07d86f Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 26 Aug 2025 21:19:49 +0200 Subject: [PATCH] refactor(nostr-outbox): add NostrOutboxService and migrate ChatViewModel geohash PM/ACK sends to it Centralize Nostr geohash outbox (PM, DELIVERED, READ) into a service wrapping NostrTransport; inject into ChatViewModel and remove ad-hoc NostrTransport instantiations. --- bitchat.xcodeproj/project.pbxproj | 6 ++++ bitchat/Services/NostrOutboxService.swift | 34 +++++++++++++++++++++++ bitchat/ViewModels/ChatViewModel.swift | 28 ++++++------------- 3 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 bitchat/Services/NostrOutboxService.swift diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 9df7e583..11685935 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */; }; ABCD00052E5CCCC300162C4C /* NostrInboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */; }; ABCD00062E5CCCC300162C4D /* NostrInboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */; }; + ABCD00082E5CCCC300162C4E /* NostrOutboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00072E5CCCC300162C4E /* NostrOutboxService.swift */; }; + ABCD00092E5CCCC300162C4F /* NostrOutboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00072E5CCCC300162C4E /* NostrOutboxService.swift */; }; 0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; }; 0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; }; 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; @@ -219,6 +221,7 @@ 049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeduplicator.swift; sourceTree = ""; }; ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptTracker.swift; sourceTree = ""; }; ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrInboxService.swift; sourceTree = ""; }; + ABCD00072E5CCCC300162C4E /* NostrOutboxService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrOutboxService.swift; sourceTree = ""; }; 049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerIDResolver.swift; sourceTree = ""; }; 049BD3AD2E51ED60001A566B /* Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transport.swift; sourceTree = ""; }; 049BD3B02E51F319001A566B /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = ""; }; @@ -528,6 +531,7 @@ 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */, ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */, ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */, + ABCD00072E5CCCC300162C4E /* NostrOutboxService.swift */, AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */, 047502B82E560F690083520F /* RelayController.swift */, 0475028B2E54171C0083520F /* LocationChannelManager.swift */, @@ -753,6 +757,7 @@ 048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */, ABCD00022E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */, ABCD00052E5CCCC300162C4C /* NostrInboxService.swift in Sources */, + ABCD00082E5CCCC300162C4E /* NostrOutboxService.swift in Sources */, 1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */, AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */, AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */, @@ -814,6 +819,7 @@ 048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */, ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */, ABCD00062E5CCCC300162C4D /* NostrInboxService.swift in Sources */, + ABCD00092E5CCCC300162C4F /* NostrOutboxService.swift in Sources */, 1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */, AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */, AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */, diff --git a/bitchat/Services/NostrOutboxService.swift b/bitchat/Services/NostrOutboxService.swift new file mode 100644 index 00000000..151b1298 --- /dev/null +++ b/bitchat/Services/NostrOutboxService.swift @@ -0,0 +1,34 @@ +// +// NostrOutboxService.swift +// bitchat +// +// Centralized Nostr outbox for sending PMs and ACKs. +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +@MainActor +final class NostrOutboxService { + private let transport: NostrTransport + + init(meshService: Transport) { + let t = NostrTransport() + t.senderPeerID = meshService.myPeerID + self.transport = t + } + + func sendGeohashPM(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) { + transport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: identity, messageID: messageID) + } + + func sendGeohashDeliveredAck(messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { + transport.sendDeliveryAckGeohash(for: messageID, toRecipientHex: recipientHex, from: identity) + } + + func sendGeohashReadAck(messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { + transport.sendReadReceiptGeohash(messageID, toRecipientHex: recipientHex, from: identity) + } +} + diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 72f85f31..2373e190 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -358,6 +358,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { private var currentGeohash: String? = nil private var geoNicknames: [String: String] = [:] // pubkeyHex(lowercased) -> nickname private let nostrInbox = NostrInboxService() + private var nostrOutbox: NostrOutboxService // MARK: - Caches @@ -463,6 +464,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { self.unifiedPeerService = UnifiedPeerService(meshService: meshService) let nostrTransport = NostrTransport() self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport) + self.nostrOutbox = NostrOutboxService(meshService: meshService) // Route receipts from PrivateChatManager through MessageRouter self.privateChatManager.messageRouter = self.messageRouter self.autocompleteService = AutocompleteService() @@ -707,6 +709,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { self.messageRouter = messageRouter self.readReceiptTracker = readReceiptTracker self.autocompleteService = AutocompleteService() + self.nostrOutbox = NostrOutboxService(meshService: meshService) // Wire up dependencies self.commandProcessor.chatViewModel = self @@ -986,9 +989,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { let messageId = pm.messageID // Send delivery ACK immediately (once per message ID) if !self.sentGeoDeliveryAcks.contains(messageId) { - let nt = NostrTransport() - nt.senderPeerID = self.meshService.myPeerID - nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) + self.nostrOutbox.sendGeohashDeliveredAck(messageID: messageId, toRecipientHex: senderPubkey, from: id) self.sentGeoDeliveryAcks.insert(messageId) } // Dedup storage @@ -1024,9 +1025,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { if isViewing { // pared back: omit pre-send READ log if !wasReadBefore { - let nt = NostrTransport() - nt.senderPeerID = self.meshService.myPeerID - nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) + self.nostrOutbox.sendGeohashReadAck(messageID: messageId, toRecipientHex: senderPubkey, from: id) self.readReceiptTracker.insert(messageId) } } else { @@ -1663,10 +1662,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { category: SecureLogger.session, level: .info) // Send delivery ACK immediately (even if duplicate), once per messageID if !self.sentGeoDeliveryAcks.contains(messageId) { - let nostrTransport = NostrTransport() - nostrTransport.senderPeerID = self.meshService.myPeerID - // pared back: omit pre-send log - nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) + self.nostrOutbox.sendGeohashDeliveredAck(messageID: messageId, toRecipientHex: senderPubkey, from: id) self.sentGeoDeliveryAcks.insert(messageId) } // Duplicate check @@ -1698,9 +1694,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { if isViewing { // pared back: omit pre-send READ log if !wasReadBefore { - let nostrTransport = NostrTransport() - nostrTransport.senderPeerID = self.meshService.myPeerID - nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) + self.nostrOutbox.sendGeohashReadAck(messageID: messageId, toRecipientHex: senderPubkey, from: id) self.readReceiptTracker.insert(messageId) } } else { @@ -2059,9 +2053,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { } SecureLogger.log("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: SecureLogger.session, level: .debug) - let nostrTransport = NostrTransport() - nostrTransport.senderPeerID = meshService.myPeerID - nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID) + self.nostrOutbox.sendGeohashPM(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID) if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) { privateChats[peerID]?[msgIdx].deliveryStatus = .sent } @@ -2773,9 +2765,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { if !readReceiptTracker.contains(message.id) { SecureLogger.log("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: SecureLogger.session, level: .debug) - let nostrTransport = NostrTransport() - nostrTransport.senderPeerID = meshService.myPeerID - nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id) + self.nostrOutbox.sendGeohashReadAck(messageID: message.id, toRecipientHex: recipientHex, from: id) readReceiptTracker.insert(message.id) } }