From 4a382716bbca50c22ed4f77e380485000bf4837c Mon Sep 17 00:00:00 2001 From: islam <2553451+qalandarov@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:15:45 +0100 Subject: [PATCH] Break down / flatten `resubscribeCurrentGeohash` --- bitchat/ViewModels/ChatViewModel.swift | 237 +++++++++++++------------ 1 file changed, 122 insertions(+), 115 deletions(-) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 1e25ffc2..6d1fdc93 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -916,111 +916,18 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { self?.subscribeNostrEvent(event) } // Resubscribe geohash DMs for this identity - if let dmSub = geoDmSubscriptionID { NostrRelayManager.shared.unsubscribe(id: dmSub); geoDmSubscriptionID = nil } - do { - let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) + if let dmSub = geoDmSubscriptionID { + NostrRelayManager.shared.unsubscribe(id: dmSub); geoDmSubscriptionID = nil + } + + if let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) { let dmSub = "geo-dm-\(ch.geohash)" geoDmSubscriptionID = dmSub let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds)) NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in - guard let self = self else { return } - if self.processedNostrEvents.contains(giftWrap.id) { return } - self.recordProcessedEvent(giftWrap.id) - guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else { return } - guard content.hasPrefix("bitchat1:") else { return } - guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), - let packet = BitchatPacket.from(packetData) else { return } - guard packet.type == MessageType.noiseEncrypted.rawValue else { return } - guard let noisePayload = NoisePayload.decode(packet.payload) else { return } - let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTs)) - let convKey = "nostr_" + String(senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)) - self.nostrKeyMapping[convKey] = senderPubkey - switch noisePayload.type { - case .privateMessage: - guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return } - let messageId = pm.messageID - // Send delivery ACK immediately (once per message ID) - if !self.sentGeoDeliveryAcks.contains(messageId) { - let nt = NostrTransport(keychain: keychain) - nt.senderPeerID = self.meshService.myPeerID - nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) - self.sentGeoDeliveryAcks.insert(messageId) - } - // Dedup storage - if self.privateChats[convKey]?.contains(where: { $0.id == messageId }) == true { return } - for (_, arr) in self.privateChats { if arr.contains(where: { $0.id == messageId }) { return } } - let senderName = self.displayNameForNostrPubkey(senderPubkey) - let isViewing = (self.selectedPrivateChatPeer == convKey) - // pared back: omit view-state log - let wasReadBefore = self.sentReadReceipts.contains(messageId) - let isRecentMessage = Date().timeIntervalSince(messageTimestamp) < 30 - let shouldMarkUnread = !wasReadBefore && !isViewing && isRecentMessage - let msg = BitchatMessage( - id: messageId, - sender: senderName, - content: pm.content, - timestamp: messageTimestamp, - isRelay: false, - originalSender: nil, - isPrivate: true, - recipientNickname: self.nickname, - senderPeerID: convKey, - mentions: nil, - deliveryStatus: .delivered(to: self.nickname, at: Date()) - ) - // Respect geohash blocks - if identityManager.isNostrBlocked(pubkeyHexLowercased: senderPubkey) { - return - } - if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] } - self.privateChats[convKey]?.append(msg) - self.trimPrivateChatMessagesIfNeeded(for: convKey) - if shouldMarkUnread { self.unreadPrivateMessages.insert(convKey) } - if isViewing { - // pared back: omit pre-send READ log - if !wasReadBefore { - let nt = NostrTransport(keychain: keychain) - nt.senderPeerID = self.meshService.myPeerID - nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) - self.sentReadReceipts.insert(messageId) - } - } else { - // Notify for truly unread and recent messages when not viewing - if shouldMarkUnread { - NotificationService.shared.sendPrivateMessageNotification( - from: senderName, - message: pm.content, - peerID: convKey - ) - } - } - self.objectWillChange.send() - case .delivered: - if let messageID = String(data: noisePayload.data, encoding: .utf8) { - if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { - self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date()) - self.objectWillChange.send() - SecureLogger.info("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session) - } else { - SecureLogger.warning("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) - } - } - case .readReceipt: - if let messageID = String(data: noisePayload.data, encoding: .utf8) { - if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { - self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date()) - self.objectWillChange.send() - SecureLogger.info("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session) - } else { - SecureLogger.warning("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) - } - } - case .verifyChallenge, .verifyResponse: - // QR verification payloads over Nostr are not supported; ignore in geohash DMs - break - } + self?.subscribeGiftWrap(giftWrap, id: id) } - } catch { } + } } private func subscribeNostrEvent(_ event: NostrEvent) { @@ -1099,7 +1006,119 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { sendHapticFeedback(for: msg) } } - + + private func subscribeGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) { + guard !processedNostrEvents.contains(giftWrap.id) else { return } + recordProcessedEvent(giftWrap.id) + + guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id), + content.hasPrefix("bitchat1:"), + let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), + let packet = BitchatPacket.from(packetData), + packet.type == MessageType.noiseEncrypted.rawValue, + let noisePayload = NoisePayload.decode(packet.payload) + else { + return + } + + let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTs)) + let convKey = "nostr_" + String(senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)) + nostrKeyMapping[convKey] = senderPubkey + + switch noisePayload.type { + case .privateMessage: + handlePrivateMessage(payload: noisePayload, senderPubkey: senderPubkey, convKey: convKey, id: id, messageTimestamp: messageTimestamp) + case .delivered: + handleDelivered(noisePayload, senderPubkey: senderPubkey, convKey: convKey) + case .readReceipt: + handleReadReceipt(noisePayload, senderPubkey: senderPubkey, convKey: convKey) + case .verifyChallenge, .verifyResponse: + // QR verification payloads over Nostr are not supported; ignore in geohash DMs + break + } + } + + private func handlePrivateMessage( + payload: NoisePayload, + senderPubkey: String, + convKey: String, + id: NostrIdentity, + messageTimestamp: Date + ) { + guard let pm = PrivateMessagePacket.decode(from: payload.data) else { return } + let messageId = pm.messageID + + sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id) + + // Respect geohash blocks + if identityManager.isNostrBlocked(pubkeyHexLowercased: senderPubkey) { + return + } + + // Dedup storage + if privateChats[convKey]?.contains(where: { $0.id == messageId }) == true { return } + for (_, arr) in privateChats { if arr.contains(where: { $0.id == messageId }) { return } } + let senderName = displayNameForNostrPubkey(senderPubkey) + + let msg = BitchatMessage( + id: messageId, + sender: senderName, + content: pm.content, + timestamp: messageTimestamp, + isRelay: false, + isPrivate: true, + recipientNickname: nickname, + senderPeerID: convKey, + deliveryStatus: .delivered(to: nickname, at: Date()) + ) + + if privateChats[convKey] == nil { + privateChats[convKey] = [] + } + privateChats[convKey]?.append(msg) + trimPrivateChatMessagesIfNeeded(for: convKey) + + // pared back: omit view-state log + let isViewing = selectedPrivateChatPeer == convKey + let wasReadBefore = sentReadReceipts.contains(messageId) + let isRecentMessage = Date().timeIntervalSince(messageTimestamp) < 30 + let shouldMarkUnread = !wasReadBefore && !isViewing && isRecentMessage + if shouldMarkUnread { + unreadPrivateMessages.insert(convKey) + } + + if isViewing { + // pared back: omit pre-send READ log + sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id) + } else { + // Notify for truly unread and recent messages when not viewing + if shouldMarkUnread { + NotificationService.shared.sendPrivateMessageNotification( + from: senderName, + message: pm.content, + peerID: convKey + ) + } + } + objectWillChange.send() + } + + private func sendDeliveryAckIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) { + guard !sentGeoDeliveryAcks.contains(messageId) else { return } + let nt = NostrTransport(keychain: keychain) + nt.senderPeerID = meshService.myPeerID + nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubKey, from: id) + sentGeoDeliveryAcks.insert(messageId) + } + + private func sendReadReceiptIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) { + guard !sentReadReceipts.contains(messageId) else { return } + let nt = NostrTransport(keychain: keychain) + nt.senderPeerID = meshService.myPeerID + nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubKey, from: id) + sentReadReceipts.insert(messageId) + } + // MARK: - Nickname Management private func loadNickname() { @@ -1781,14 +1800,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session) - // Send delivery ACK immediately (even if duplicate), once per messageID - if !sentGeoDeliveryAcks.contains(messageId) { - let nostrTransport = NostrTransport(keychain: keychain) - nostrTransport.senderPeerID = meshService.myPeerID - // pared back: omit pre-send log - nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) - sentGeoDeliveryAcks.insert(messageId) - } + sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id) // Duplicate check if privateChats[convKey]?.contains(where: { $0.id == messageId }) == true { return } @@ -1828,12 +1840,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Send READ if viewing this conversation if isViewing { // pared back: omit pre-send READ log - if !wasReadBefore { - let nostrTransport = NostrTransport(keychain: keychain) - nostrTransport.senderPeerID = meshService.myPeerID - nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) - sentReadReceipts.insert(messageId) - } + sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id) } else { // pared back: omit defer READ log }