mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 17:19:27 +02:00
feat(chat): improve NIP-10/NIP-22 UI and relay selection
ChatViewer improvements: - Make NIP-22 protocol badge clickable to open NIP specification - Hide "load older messages" button for NIP-10 and NIP-22 (thread-based protocols) - Show custom title with kind icon, author, and event title for thread-based chats NIP-22 relay selection improvements: - Extract relay hints from comment event tags (A/E tags with relay URLs) - Fetch author's outbox relays (kind 10002) for better event discovery - Merge relay hints from multiple sources: pointer, comment tags, author outbox - Should significantly reduce "Comment root not found" errors All 1114 tests passing.
This commit is contained in:
@@ -69,6 +69,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
import { useBlossomUpload } from "@/hooks/useBlossomUpload";
|
||||
import { getKindIcon } from "@/constants/kinds";
|
||||
import { getEventDisplayTitle } from "@/lib/event-title";
|
||||
|
||||
interface ChatViewerProps {
|
||||
protocol: ChatProtocol;
|
||||
@@ -831,6 +833,8 @@ export function ChatViewer({
|
||||
const handleNipClick = useCallback(() => {
|
||||
if (conversation?.protocol === "nip-10") {
|
||||
addWindow("nip", { number: 10 });
|
||||
} else if (conversation?.protocol === "nip-22") {
|
||||
addWindow("nip", { number: 22 });
|
||||
} else if (conversation?.protocol === "nip-29") {
|
||||
addWindow("nip", { number: 29 });
|
||||
} else if (conversation?.protocol === "nip-53") {
|
||||
@@ -906,6 +910,30 @@ export function ChatViewer({
|
||||
liveActivity?.hostPubkey,
|
||||
]);
|
||||
|
||||
// Get root event for NIP-10 and NIP-22 to build custom title
|
||||
// Must be called before any early returns (React Hooks rules)
|
||||
const rootEventId =
|
||||
(protocol === "nip-10" || protocol === "nip-22") && conversation
|
||||
? conversation.metadata?.rootEventId ||
|
||||
conversation.metadata?.providedEventId
|
||||
: undefined;
|
||||
|
||||
const rootEvent = use$(
|
||||
() => (rootEventId ? eventStore.event(rootEventId) : undefined),
|
||||
[rootEventId],
|
||||
);
|
||||
|
||||
// Compute custom title for thread-based protocols (NIP-10, NIP-22)
|
||||
const threadTitle = useMemo(() => {
|
||||
if ((protocol !== "nip-10" && protocol !== "nip-22") || !rootEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const kindIcon = getKindIcon(rootEvent.kind);
|
||||
const eventTitle = getEventDisplayTitle(rootEvent);
|
||||
return { kindIcon, author: rootEvent.pubkey, eventTitle };
|
||||
}, [protocol, rootEvent]);
|
||||
|
||||
// Handle loading state
|
||||
if (!conversationResult || conversationResult.status === "loading") {
|
||||
return (
|
||||
@@ -951,10 +979,28 @@ export function ChatViewer({
|
||||
<Tooltip open={tooltipOpen} onOpenChange={setTooltipOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="text-sm font-semibold truncate cursor-help text-left"
|
||||
className="text-sm font-semibold truncate cursor-help text-left flex items-center gap-1.5 min-w-0"
|
||||
onClick={() => setTooltipOpen(!tooltipOpen)}
|
||||
>
|
||||
{customTitle || conversation.title}
|
||||
{threadTitle ? (
|
||||
<>
|
||||
{(() => {
|
||||
const KindIcon = threadTitle.kindIcon;
|
||||
return KindIcon ? (
|
||||
<KindIcon className="size-4 flex-shrink-0" />
|
||||
) : null;
|
||||
})()}
|
||||
<UserName
|
||||
pubkey={threadTitle.author}
|
||||
className="text-sm font-semibold flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
{threadTitle.eventTitle}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
customTitle || conversation.title
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
@@ -988,7 +1034,9 @@ export function ChatViewer({
|
||||
)}
|
||||
{/* Protocol Type - Clickable */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{(conversation.type === "group" ||
|
||||
{(conversation.protocol === "nip-10" ||
|
||||
conversation.protocol === "nip-22" ||
|
||||
conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -1000,11 +1048,14 @@ export function ChatViewer({
|
||||
{conversation.protocol.toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
{(conversation.type === "group" ||
|
||||
{(conversation.protocol === "nip-10" ||
|
||||
conversation.protocol === "nip-22" ||
|
||||
conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<span className="opacity-60">•</span>
|
||||
)}
|
||||
{conversation.protocol === "nip-10" ? (
|
||||
{conversation.protocol === "nip-10" ||
|
||||
conversation.protocol === "nip-22" ? (
|
||||
<span className="flex items-center gap-1 opacity-80">
|
||||
<FileText className="size-3" />
|
||||
Thread
|
||||
@@ -1057,7 +1108,9 @@ export function ChatViewer({
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
|
||||
<MembersDropdown participants={derivedParticipants} />
|
||||
<RelaysDropdown conversation={conversation} />
|
||||
{(conversation.type === "group" ||
|
||||
{(conversation.protocol === "nip-10" ||
|
||||
conversation.protocol === "nip-22" ||
|
||||
conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={handleNipClick}
|
||||
@@ -1083,7 +1136,8 @@ export function ChatViewer({
|
||||
Header: () =>
|
||||
hasMore &&
|
||||
conversationResult.status === "success" &&
|
||||
protocol !== "nip-10" ? (
|
||||
protocol !== "nip-10" &&
|
||||
protocol !== "nip-22" ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
|
||||
@@ -22,8 +22,7 @@ import pool from "@/services/relay-pool";
|
||||
import { publishEventToRelays } from "@/services/hub";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { getOutboxes } from "applesauce-core/helpers/mailboxes";
|
||||
import { mergeRelaySets, getOutboxes } from "applesauce-core/helpers";
|
||||
import {
|
||||
getEventPointerFromETag,
|
||||
parseReplaceableAddress,
|
||||
@@ -170,6 +169,7 @@ export class Nip22Adapter extends ChatProtocolAdapter {
|
||||
const fetchedRoot = await this.fetchEventByPointer(
|
||||
eventPointer,
|
||||
relayHints,
|
||||
providedEvent, // Pass comment event for relay hint extraction
|
||||
);
|
||||
if (!fetchedRoot) {
|
||||
throw new Error("Comment root not found");
|
||||
@@ -192,6 +192,7 @@ export class Nip22Adapter extends ChatProtocolAdapter {
|
||||
const fetchedRoot = await this.fetchAddressableEvent(
|
||||
addressPointer,
|
||||
relayHints,
|
||||
providedEvent, // Pass comment event for relay hint extraction
|
||||
);
|
||||
if (!fetchedRoot) {
|
||||
throw new Error("Comment root not found");
|
||||
@@ -868,22 +869,95 @@ export class Nip22Adapter extends ChatProtocolAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Fetch event by EventPointer
|
||||
* Helper: Extract relay hints from comment event tags
|
||||
* NIP-22 comments (kind 1111) may have relay hints in A/E tags
|
||||
*/
|
||||
private extractRelayHintsFromComment(
|
||||
comment: NostrEvent,
|
||||
targetEventId?: string,
|
||||
targetCoordinate?: string,
|
||||
): string[] {
|
||||
const relays: string[] = [];
|
||||
|
||||
// Look for A-tag (addressable root) with relay hint
|
||||
if (targetCoordinate) {
|
||||
for (const tag of comment.tags) {
|
||||
if (
|
||||
(tag[0] === "A" || tag[0] === "a") &&
|
||||
tag[1] === targetCoordinate &&
|
||||
tag[2]
|
||||
) {
|
||||
relays.push(tag[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for E-tag (regular event root) with relay hint
|
||||
if (targetEventId) {
|
||||
for (const tag of comment.tags) {
|
||||
if (
|
||||
(tag[0] === "E" || tag[0] === "e") &&
|
||||
tag[1] === targetEventId &&
|
||||
tag[2]
|
||||
) {
|
||||
relays.push(tag[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Fetch event by EventPointer with improved relay selection
|
||||
* Uses: relay hints from pointer, relay hints from comment tags, author's outbox relays
|
||||
*/
|
||||
private async fetchEventByPointer(
|
||||
pointer: EventPointer,
|
||||
additionalHints: string[] = [],
|
||||
commentEvent?: NostrEvent,
|
||||
): Promise<NostrEvent | null> {
|
||||
const relayHints = mergeRelaySets(pointer.relays || [], additionalHints);
|
||||
return this.fetchEvent({ id: pointer.id, kind: pointer.kind }, relayHints);
|
||||
// Merge relay hints from multiple sources
|
||||
const pointerHints = pointer.relays || [];
|
||||
const commentHints = commentEvent
|
||||
? this.extractRelayHintsFromComment(commentEvent, pointer.id)
|
||||
: [];
|
||||
|
||||
// Get author's outbox relays if we know the author
|
||||
let authorOutbox: string[] = [];
|
||||
if (pointer.author) {
|
||||
try {
|
||||
const relayListEvent = await firstValueFrom(
|
||||
eventStore.replaceable(10002, pointer.author, ""),
|
||||
{ defaultValue: undefined },
|
||||
);
|
||||
if (relayListEvent) {
|
||||
authorOutbox = getOutboxes(relayListEvent);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors fetching relay list
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all hints (prioritize pointer hints, then comment hints, then author outbox)
|
||||
const allHints = mergeRelaySets(
|
||||
pointerHints,
|
||||
commentHints,
|
||||
authorOutbox.slice(0, 3), // Limit outbox to 3 relays
|
||||
additionalHints,
|
||||
);
|
||||
|
||||
return this.fetchEvent({ id: pointer.id, kind: pointer.kind }, allHints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Fetch addressable event by AddressPointer
|
||||
* Helper: Fetch addressable event by AddressPointer with improved relay selection
|
||||
* Uses: relay hints from pointer, relay hints from comment tags, author's outbox relays
|
||||
*/
|
||||
private async fetchAddressableEvent(
|
||||
pointer: AddressPointer,
|
||||
additionalHints: string[] = [],
|
||||
commentEvent?: NostrEvent,
|
||||
): Promise<NostrEvent | null> {
|
||||
const { kind, pubkey, identifier } = pointer;
|
||||
|
||||
@@ -894,10 +968,37 @@ export class Nip22Adapter extends ChatProtocolAdapter {
|
||||
);
|
||||
if (cached) return cached;
|
||||
|
||||
// Not in store - fetch from relays
|
||||
const relayHints = mergeRelaySets(pointer.relays || [], additionalHints);
|
||||
// Merge relay hints from multiple sources
|
||||
const pointerHints = pointer.relays || [];
|
||||
const coordinate = `${kind}:${pubkey}:${identifier}`;
|
||||
const commentHints = commentEvent
|
||||
? this.extractRelayHintsFromComment(commentEvent, undefined, coordinate)
|
||||
: [];
|
||||
|
||||
// Get author's outbox relays
|
||||
let authorOutbox: string[] = [];
|
||||
try {
|
||||
const relayListEvent = await firstValueFrom(
|
||||
eventStore.replaceable(10002, pubkey, ""),
|
||||
{ defaultValue: undefined },
|
||||
);
|
||||
if (relayListEvent) {
|
||||
authorOutbox = getOutboxes(relayListEvent);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors fetching relay list
|
||||
}
|
||||
|
||||
// Merge all hints (prioritize pointer hints, then comment hints, then author outbox)
|
||||
const allHints = mergeRelaySets(
|
||||
pointerHints,
|
||||
commentHints,
|
||||
authorOutbox.slice(0, 3), // Limit outbox to 3 relays
|
||||
additionalHints,
|
||||
);
|
||||
|
||||
const relays =
|
||||
relayHints.length > 0 ? relayHints : await this.getDefaultRelays();
|
||||
allHints.length > 0 ? allHints : await this.getDefaultRelays();
|
||||
|
||||
const filter: Filter = {
|
||||
kinds: [kind],
|
||||
|
||||
Reference in New Issue
Block a user