mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:45:19 +00:00
Break down / flatten resubscribeCurrentGeohash
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user