diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 31b2487..7b1224d 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -12,7 +12,6 @@ import { DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, - DropdownMenuCheckboxItem, } from "@/components/ui/dropdown-menu"; import { Menu, @@ -29,7 +28,7 @@ import { JsonViewer } from "@/components/JsonViewer"; import { formatTimestamp } from "@/hooks/useLocale"; import { nip19 } from "nostr-tools"; import { getTagValue } from "applesauce-core/helpers"; -import { getSeenRelays } from "applesauce-core/helpers/relays"; +import { getSeenRelays, addSeenRelay } from "applesauce-core/helpers/relays"; import { EventFooter } from "@/components/EventFooter"; import { cn } from "@/lib/utils"; import { isAddressableKind } from "@/lib/nostr-kinds"; @@ -39,6 +38,7 @@ import accountManager from "@/services/accounts"; import { toast } from "sonner"; import { use$ } from "applesauce-react/hooks"; import { Button } from "@/components/ui/button"; +import { useRelayInfo } from "@/hooks/useRelayInfo"; /** * Universal event properties and utilities shared across all kind renderers @@ -115,6 +115,80 @@ function ReplyPreview({ } */ +/** + * Format relay URL for display by removing protocol and trailing slashes + */ +function formatRelayUrlForDisplay(url: string): string { + return url + .replace(/^wss?:\/\//, "") // Remove ws:// or wss:// + .replace(/\/$/, ""); // Remove trailing slash +} + +/** + * RelayPublishItem - Clickable relay item for republish submenu + */ +function RelayPublishItem({ + url, + isPublishing, + isPublished, + onClick, +}: { + url: string; + isPublishing: boolean; + isPublished: boolean; + onClick: () => void; +}) { + const relayInfo = useRelayInfo(url); + const displayUrl = formatRelayUrlForDisplay(url); + + return ( + + {/* Relay icon and info */} + + {relayInfo?.icon ? ( + + ) : ( + + )} + + {relayInfo?.name && ( + + {relayInfo.name} + + )} + + {displayUrl} + + + + + {/* Status icon */} + + {isPublishing ? ( + + ) : isPublished ? ( + + ) : ( + + )} + + + ); +} + /** * Event menu - universal actions for any event */ @@ -123,8 +197,12 @@ export function EventMenu({ event }: { event: NostrEvent }) { const { copy, copied } = useCopy(); const [jsonDialogOpen, setJsonDialogOpen] = useState(false); const [myRelays, setMyRelays] = useState([]); - const [selectedRelays, setSelectedRelays] = useState>(new Set()); - const [isPublishing, setIsPublishing] = useState(false); + const [publishingRelays, setPublishingRelays] = useState>( + new Set(), + ); + const [publishedRelays, setPublishedRelays] = useState>( + new Set(), + ); const account = use$(accountManager.active$); // Get user's outbox relays and seen relays @@ -143,7 +221,7 @@ export function EventMenu({ event }: { event: NostrEvent }) { }); }, [account]); - // Combine and deduplicate relays for the checkbox list + // Combine and deduplicate relays for the list const allRelays = Array.from(new Set([...myRelays, ...seenRelays])); const openEventDetail = () => { @@ -204,9 +282,18 @@ export function EventMenu({ event }: { event: NostrEvent }) { return; } - setIsPublishing(true); + // Mark all relays as publishing + setPublishingRelays(new Set(myRelays)); + try { await publishEventToRelays(event, myRelays); + + // Mark event as seen on all relays after successful publish + myRelays.forEach((relay) => addSeenRelay(event, relay)); + + // Mark all as published + setPublishedRelays(new Set(myRelays)); + toast.success( `Published to ${myRelays.length} relay${myRelays.length > 1 ? "s" : ""}`, ); @@ -215,43 +302,36 @@ export function EventMenu({ event }: { event: NostrEvent }) { `Failed to publish: ${error instanceof Error ? error.message : "Unknown error"}`, ); } finally { - setIsPublishing(false); + setPublishingRelays(new Set()); } }; - const handleRepublishToSelected = async () => { - const relaysArray = Array.from(selectedRelays); - if (relaysArray.length === 0) { - toast.error("No relays selected"); - return; - } + const handleRepublishToRelay = async (relay: string) => { + // Mark this relay as publishing + setPublishingRelays((prev) => new Set([...prev, relay])); - setIsPublishing(true); try { - await publishEventToRelays(event, relaysArray); - toast.success( - `Published to ${relaysArray.length} relay${relaysArray.length > 1 ? "s" : ""}`, - ); - setSelectedRelays(new Set()); // Clear selection after successful publish + await publishEventToRelays(event, [relay]); + + // Mark event as seen on this relay after successful publish + addSeenRelay(event, relay); + + // Mark as published + setPublishedRelays((prev) => new Set([...prev, relay])); + + toast.success("Published successfully"); } catch (error) { toast.error( `Failed to publish: ${error instanceof Error ? error.message : "Unknown error"}`, ); } finally { - setIsPublishing(false); - } - }; - - const toggleRelay = (relay: string) => { - setSelectedRelays((prev) => { - const next = new Set(prev); - if (next.has(relay)) { + // Remove from publishing set + setPublishingRelays((prev) => { + const next = new Set(prev); next.delete(relay); - } else { - next.add(relay); - } - return next; - }); + return next; + }); + } }; return ( @@ -281,39 +361,30 @@ export function EventMenu({ event }: { event: NostrEvent }) { {/* Republish submenu */} - - {isPublishing ? ( - - ) : ( - - )} + + Republish - + {/* Quick action: Republish to my relays */} {account && myRelays.length > 0 && ( <> - + 0} > - {isPublishing ? ( + {publishingRelays.size > 0 ? ( ) : ( )} - My relays ({myRelays.length}) + Publish to all my relays ({myRelays.length}) - - Select relays - > )} @@ -324,65 +395,45 @@ export function EventMenu({ event }: { event: NostrEvent }) { )} - {/* Checkbox list: My relays */} + {/* My relays list */} {account && myRelays.length > 0 && ( <> - + My relays - {myRelays.map((relay) => ( - toggleRelay(relay)} - onSelect={(e) => e.preventDefault()} - > - {relay} - - ))} + + {myRelays.map((relay) => ( + handleRepublishToRelay(relay)} + /> + ))} + > )} - {/* Checkbox list: Connected relays (seen relays not in my relays) */} + {/* Connected relays (seen relays not in my relays) */} {seenRelays.filter((r) => !myRelays.includes(r)).length > 0 && ( <> - + {account && myRelays.length > 0 && } + Connected relays - {seenRelays - .filter((r) => !myRelays.includes(r)) - .map((relay) => ( - toggleRelay(relay)} - onSelect={(e) => e.preventDefault()} - > - {relay} - - ))} - > - )} - - {/* Publish button for selected relays */} - {selectedRelays.size > 0 && ( - <> - - - - {isPublishing ? ( - - ) : ( - - )} - Publish to {selectedRelays.size} selected - + + {seenRelays + .filter((r) => !myRelays.includes(r)) + .map((relay) => ( + handleRepublishToRelay(relay)} + /> + ))} > )}