mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-06 18:51:21 +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 { getSemanticAuthor } from "@/lib/semantic-author";
|
||||||
import { EventFactory } from "applesauce-core/event-factory";
|
import { EventFactory } from "applesauce-core/event-factory";
|
||||||
import { ReactionBlueprint } from "applesauce-common/blueprints";
|
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";
|
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -529,7 +530,7 @@ export function BaseEventContainer({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { locale, addWindow } = useGrimoire();
|
const { locale, addWindow } = useGrimoire();
|
||||||
const { canSign, signer } = useAccount();
|
const { canSign, signer, pubkey } = useAccount();
|
||||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||||
|
|
||||||
const handleReactClick = () => {
|
const handleReactClick = () => {
|
||||||
@@ -537,7 +538,7 @@ export function BaseEventContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEmojiSelect = async (emoji: string, customEmoji?: EmojiTag) => {
|
const handleEmojiSelect = async (emoji: string, customEmoji?: EmojiTag) => {
|
||||||
if (!signer) return;
|
if (!signer || !pubkey) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const factory = new EventFactory();
|
const factory = new EventFactory();
|
||||||
@@ -549,7 +550,10 @@ export function BaseEventContainer({
|
|||||||
|
|
||||||
const draft = await factory.create(ReactionBlueprint, event, emojiArg);
|
const draft = await factory.create(ReactionBlueprint, event, emojiArg);
|
||||||
const signed = await factory.sign(draft);
|
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) {
|
} catch (err) {
|
||||||
console.error("[BaseEventContainer] Failed to send reaction:", 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