mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 15:36:53 +02:00
feat: use NIP-65 relay selection for reactions
Add interaction relay selection utility following the NIP-65 outbox model: - Author's outbox (write) relays: where we publish our events - Target's inbox (read) relays: so the target sees the interaction This ensures reactions reach the intended recipient according to their relay preferences, similar to how zap relay selection works. New file: src/lib/interaction-relay-selection.ts - selectInteractionRelays() for full result with sources - getInteractionRelays() convenience wrapper
This commit is contained in:
@@ -41,7 +41,8 @@ import { isAddressableKind } from "@/lib/nostr-kinds";
|
||||
import { getSemanticAuthor } from "@/lib/semantic-author";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import { ReactionBlueprint } from "applesauce-common/blueprints";
|
||||
import { publishEvent } from "@/services/hub";
|
||||
import { publishEventToRelays } from "@/services/hub";
|
||||
import { getInteractionRelays } from "@/lib/interaction-relay-selection";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
|
||||
/**
|
||||
@@ -529,7 +530,7 @@ export function BaseEventContainer({
|
||||
};
|
||||
}) {
|
||||
const { locale, addWindow } = useGrimoire();
|
||||
const { canSign, signer } = useAccount();
|
||||
const { canSign, signer, pubkey } = useAccount();
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
const handleReactClick = () => {
|
||||
@@ -537,7 +538,7 @@ export function BaseEventContainer({
|
||||
};
|
||||
|
||||
const handleEmojiSelect = async (emoji: string, customEmoji?: EmojiTag) => {
|
||||
if (!signer) return;
|
||||
if (!signer || !pubkey) return;
|
||||
|
||||
try {
|
||||
const factory = new EventFactory();
|
||||
@@ -549,7 +550,10 @@ export function BaseEventContainer({
|
||||
|
||||
const draft = await factory.create(ReactionBlueprint, event, emojiArg);
|
||||
const signed = await factory.sign(draft);
|
||||
await publishEvent(signed);
|
||||
|
||||
// Select relays per NIP-65: author's outbox + target's inbox
|
||||
const relays = await getInteractionRelays(pubkey, event.pubkey);
|
||||
await publishEventToRelays(signed, relays);
|
||||
} catch (err) {
|
||||
console.error("[BaseEventContainer] Failed to send reaction:", err);
|
||||
}
|
||||
|
||||
128
src/lib/interaction-relay-selection.ts
Normal file
128
src/lib/interaction-relay-selection.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Interaction Relay Selection Utilities
|
||||
*
|
||||
* Provides optimal relay selection for interactions (reactions, replies, etc.)
|
||||
* following the NIP-65 outbox model.
|
||||
*
|
||||
* For interactions, we need to publish to:
|
||||
* 1. Author's outbox (write) relays - where we publish our events
|
||||
* 2. Target's inbox (read) relays - so the target sees the interaction
|
||||
*
|
||||
* See: https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||
*/
|
||||
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
|
||||
/** Maximum number of relays to publish to */
|
||||
const MAX_INTERACTION_RELAYS = 10;
|
||||
|
||||
/** Minimum relays per party for redundancy */
|
||||
const MIN_RELAYS_PER_PARTY = 3;
|
||||
|
||||
export interface InteractionRelaySelectionParams {
|
||||
/** Pubkey of the interaction author (person reacting/replying) */
|
||||
authorPubkey: string;
|
||||
/** Pubkey of the target (person being reacted to/replied to) */
|
||||
targetPubkey: string;
|
||||
}
|
||||
|
||||
export interface InteractionRelaySelectionResult {
|
||||
/** Selected relays for publishing the interaction */
|
||||
relays: string[];
|
||||
/** Debug info about relay sources */
|
||||
sources: {
|
||||
authorOutbox: string[];
|
||||
targetInbox: string[];
|
||||
fallback: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Select optimal relays for publishing an interaction event (reaction, reply, etc.)
|
||||
*
|
||||
* Strategy per NIP-65:
|
||||
* - Author's outbox relays: where we publish our content
|
||||
* - Target's inbox relays: where the target reads mentions/interactions
|
||||
* - Fallback aggregators if neither has preferences
|
||||
* - Deduplicate and limit to MAX_INTERACTION_RELAYS
|
||||
*/
|
||||
export async function selectInteractionRelays(
|
||||
params: InteractionRelaySelectionParams,
|
||||
): Promise<InteractionRelaySelectionResult> {
|
||||
const { authorPubkey, targetPubkey } = params;
|
||||
|
||||
const sources = {
|
||||
authorOutbox: [] as string[],
|
||||
targetInbox: [] as string[],
|
||||
fallback: [] as string[],
|
||||
};
|
||||
|
||||
// Fetch relays in parallel
|
||||
const [authorOutbox, targetInbox] = await Promise.all([
|
||||
relayListCache.getOutboxRelays(authorPubkey),
|
||||
relayListCache.getInboxRelays(targetPubkey),
|
||||
]);
|
||||
|
||||
if (authorOutbox && authorOutbox.length > 0) {
|
||||
sources.authorOutbox = authorOutbox;
|
||||
}
|
||||
|
||||
if (targetInbox && targetInbox.length > 0) {
|
||||
sources.targetInbox = targetInbox;
|
||||
}
|
||||
|
||||
// Build relay list with priority ordering
|
||||
const relaySet = new Set<string>();
|
||||
|
||||
// Priority 1: Author's outbox relays (where we publish)
|
||||
for (const relay of sources.authorOutbox.slice(0, MIN_RELAYS_PER_PARTY)) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Priority 2: Target's inbox relays (so they see it)
|
||||
for (const relay of sources.targetInbox.slice(0, MIN_RELAYS_PER_PARTY)) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Add remaining author outbox relays
|
||||
for (const relay of sources.authorOutbox.slice(MIN_RELAYS_PER_PARTY)) {
|
||||
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Add remaining target inbox relays
|
||||
for (const relay of sources.targetInbox.slice(MIN_RELAYS_PER_PARTY)) {
|
||||
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Fallback to aggregator relays if we don't have enough
|
||||
if (relaySet.size === 0) {
|
||||
sources.fallback = [...AGGREGATOR_RELAYS];
|
||||
for (const relay of AGGREGATOR_RELAYS) {
|
||||
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
relays: Array.from(relaySet),
|
||||
sources,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple list of relays for publishing an interaction
|
||||
* Convenience wrapper that just returns the relay URLs
|
||||
*/
|
||||
export async function getInteractionRelays(
|
||||
authorPubkey: string,
|
||||
targetPubkey: string,
|
||||
): Promise<string[]> {
|
||||
const result = await selectInteractionRelays({
|
||||
authorPubkey,
|
||||
targetPubkey,
|
||||
});
|
||||
return result.relays;
|
||||
}
|
||||
Reference in New Issue
Block a user