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.
This commit is contained in:
jack
2025-08-26 21:19:49 +02:00
parent 678795adcf
commit fb60c929eb
3 changed files with 49 additions and 19 deletions
+6
View File
@@ -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 = "<group>"; };
ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptTracker.swift; sourceTree = "<group>"; };
ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrInboxService.swift; sourceTree = "<group>"; };
ABCD00072E5CCCC300162C4E /* NostrOutboxService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrOutboxService.swift; sourceTree = "<group>"; };
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerIDResolver.swift; sourceTree = "<group>"; };
049BD3AD2E51ED60001A566B /* Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transport.swift; sourceTree = "<group>"; };
049BD3B02E51F319001A566B /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = "<group>"; };
@@ -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 */,
+34
View File
@@ -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 <https://unlicense.org>
//
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)
}
}
+9 -19
View File
@@ -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)
}
}