-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathDmReactionPublisher.swift
More file actions
111 lines (103 loc) · 4.48 KB
/
Copy pathDmReactionPublisher.swift
File metadata and controls
111 lines (103 loc) · 4.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import Foundation
/// Build + send a reaction to a DM message (kind-14): a kind-7 rumor carrying
/// `["e", targetRumorId]`, `["p", targetAuthor]`, `["k", "14"]`, wrapped in NIP-17
/// gift wraps — one per conversation participant + a self-copy — so everyone on the
/// encrypted thread sees consistent reactions. Mirrors Android
/// `DmConversationViewModel.sendReaction`. Distinct from `PrivateReactionPublisher`
/// (which targets `k=1` private replies to public notes) so the proven public-reply
/// path isn't disturbed.
@MainActor
enum DmReactionPublisher {
enum SendError: Swift.Error {
case noOwnRelays
case wrapFailed(Swift.Error)
case publishFailed
}
/// React to `message` in the conversation identified by `conversationKey` whose
/// other participants are `participants` (excluding the local user). Applies the
/// reaction optimistically to `DmRepository` before publishing.
@discardableResult
static func react(
message: DmMessage,
participants: [String],
conversationKey: String,
keypair: Keypair,
picked: PickedEmoji
) async throws -> Bool {
let ownRelays = await resolveOwnDmRelays(keypair: keypair)
guard !ownRelays.isEmpty else { throw SendError.noOwnRelays }
let rumorCreatedAt = NostrClock.now()
var tags: [[String]] = []
tags.append(["e", message.rumorId])
tags.append(["p", message.senderPubkey])
tags.append(["k", "14"])
var customEmojiUrl: String? = nil
if case .custom(let shortcode, let url) = picked {
tags.append(["emoji", shortcode, url])
customEmojiUrl = url
}
let rumor = Nip17.buildRumorRaw(
senderPubkey: keypair.pubkey,
kind: Nip17.Kind.reaction,
tags: tags,
content: picked.content,
createdAt: rumorCreatedAt
)
// Optimistic local apply — deduped by (author, emoji) in DmRepository, so the
// self-wrap echo arriving back via the kind-1059 subscription won't double-count.
let reaction = DmReaction(
authorPubkey: keypair.pubkey,
emoji: picked.content,
timestamp: rumorCreatedAt,
emojiUrl: customEmojiUrl
)
DmRepository.shared.addReaction(
conversationKey: conversationKey,
targetRumorId: message.rumorId,
reaction: reaction
)
// One wrap per recipient + self; same rumor → same rumor id on every device.
var recipients = participants
recipients.append(keypair.pubkey)
var wraps: [(wrap: NostrEvent, targets: [String])] = []
do {
for recipient in recipients {
let isSelf = recipient == keypair.pubkey
let relays = isSelf ? ownRelays : await PeerRelayResolver.resolveDmTargets(pubkey: recipient)
guard !relays.isEmpty else { continue }
let wrap = try await Nip17.wrapRumorWithSigner(
keypair: keypair,
recipientPubkey: recipient,
rumor: rumor
)
wraps.append((wrap, relays))
_ = DmRepository.shared.markGiftWrapSeen(wrap.id)
}
} catch {
throw SendError.wrapFailed(error)
}
guard !wraps.isEmpty else { throw SendError.publishFailed }
var anyAccepted = false
await withTaskGroup(of: Bool.self) { group in
for entry in wraps {
group.addTask {
let accepted = await RelayPool.publish(event: entry.wrap, to: entry.targets, timeout: 8)
return !accepted.isEmpty
}
}
for await ok in group { if ok { anyAccepted = true } }
}
guard anyAccepted else { throw SendError.publishFailed }
EmojiRepository.shared.recordUse(picked.frequencyKey)
return true
}
/// Own DM relays for the self-copy: kind-10050 if present, else NIP-65 write relays
/// (matches the lenient fallback the DM send path uses so reactions work for users
/// without an explicit kind-10050 list).
private static func resolveOwnDmRelays(keypair: Keypair) async -> [String] {
RelaySettingsRepository.shared.ensureLoaded(pubkey: keypair.pubkey)
let dm = RelaySettingsRepository.shared.dmRelays
if !dm.isEmpty { return dm }
return await RelayListRepository.shared.getWriteRelays(keypair.pubkey)
}
}