Break down / flatten resubscribeCurrentGeohash

This commit is contained in:
islam
2025-09-18 02:09:05 +01:00
parent 1563b06d73
commit 4a382716bb
+122 -115
View File
@@ -916,111 +916,18 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self?.subscribeNostrEvent(event) self?.subscribeNostrEvent(event)
} }
// Resubscribe geohash DMs for this identity // Resubscribe geohash DMs for this identity
if let dmSub = geoDmSubscriptionID { NostrRelayManager.shared.unsubscribe(id: dmSub); geoDmSubscriptionID = nil } if let dmSub = geoDmSubscriptionID {
do { NostrRelayManager.shared.unsubscribe(id: dmSub); geoDmSubscriptionID = nil
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) }
if let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
let dmSub = "geo-dm-\(ch.geohash)" let dmSub = "geo-dm-\(ch.geohash)"
geoDmSubscriptionID = dmSub geoDmSubscriptionID = dmSub
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds)) let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
guard let self = self else { return } self?.subscribeGiftWrap(giftWrap, id: id)
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
}
} }
} catch { } }
} }
private func subscribeNostrEvent(_ event: NostrEvent) { private func subscribeNostrEvent(_ event: NostrEvent) {
@@ -1099,7 +1006,119 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
sendHapticFeedback(for: msg) 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 // MARK: - Nickname Management
private func loadNickname() { 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) 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 sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id)
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)
}
// Duplicate check // Duplicate check
if privateChats[convKey]?.contains(where: { $0.id == messageId }) == true { return } 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 // Send READ if viewing this conversation
if isViewing { if isViewing {
// pared back: omit pre-send READ log // pared back: omit pre-send READ log
if !wasReadBefore { sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id)
let nostrTransport = NostrTransport(keychain: keychain)
nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
sentReadReceipts.insert(messageId)
}
} else { } else {
// pared back: omit defer READ log // pared back: omit defer READ log
} }