fix: event publication

This commit is contained in:
Alejandro Gómez
2025-12-21 21:50:09 +01:00
parent 64212121bd
commit fc63b3c685
10 changed files with 47 additions and 33 deletions

View File

@@ -35,7 +35,9 @@ export interface PublishSpellbookOptions {
* @example
* ```typescript
* // Publish via ActionHub with proper side-effect handling
* for await (const event of hub.exec(PublishSpellbook, options)) {
* const event = await lastValueFrom(hub.exec(PublishSpellbook, options));
* if (event) {
* await publishEvent(event);
* // Only mark as published AFTER successful relay publish
* await markSpellbookPublished(localId, event as SpellbookEvent);
* }

View File

@@ -71,7 +71,6 @@ import { SyntaxHighlight } from "@/components/SyntaxHighlight";
import { getConnectionIcon, getAuthIcon } from "@/lib/relay-status-utils";
import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { cn } from "@/lib/utils";
import { MemoizedCompactEventRow } from "./nostr/CompactEventRow";
import type { ViewMode } from "@/lib/req-parser";

View File

@@ -19,10 +19,11 @@ import {
markSpellbookPublished,
} from "@/services/spellbook-storage";
import { PublishSpellbook } from "@/actions/publish-spellbook";
import { hub } from "@/services/hub";
import { hub, publishEvent } from "@/services/hub";
import { createSpellbook } from "@/lib/spellbook-manager";
import { Loader2, Save, Send } from "lucide-react";
import type { SpellbookEvent } from "@/types/spell";
import { lastValueFrom } from "rxjs";
interface SaveSpellbookDialogProps {
open: boolean;
@@ -112,17 +113,22 @@ export function SaveSpellbookDialog({
// 4. Optionally publish
if (shouldPublish) {
const localId = existingSpellbook?.localId || localSpellbook.id;
// Use hub.exec() to get the event and handle side effects after successful publish
for await (const event of hub.exec(PublishSpellbook, {
state,
title,
description,
workspaceIds: selectedWorkspaces,
content: localSpellbook.content,
})) {
const event = await lastValueFrom(
hub.exec(PublishSpellbook, {
state,
title,
description,
workspaceIds: selectedWorkspaces,
content: localSpellbook.content,
}),
);
if (event) {
await publishEvent(event);
// Only mark as published AFTER successful relay publish
await markSpellbookPublished(localId, event as SpellbookEvent);
}
toast.success(
isUpdateMode
? "Spellbook updated and published to Nostr"

View File

@@ -34,16 +34,19 @@ import {
import { cn } from "@/lib/utils";
import { SaveSpellbookDialog } from "./SaveSpellbookDialog";
import { toast } from "sonner";
import { UserName } from "./nostr/UserName";
/**
* Status indicator component for spellbook state
*/
function SpellbookStatus({
owner,
isOwner,
isPublished,
isLocal,
className,
}: {
owner?: string;
isOwner: boolean;
isPublished?: boolean;
isLocal?: boolean;
@@ -62,12 +65,12 @@ function SpellbookStatus({
<User className="size-2.5" />
<span>you</span>
</span>
) : (
) : owner ? (
<span className="flex items-center gap-0.5" title="Others' spellbook">
<Users className="size-2.5" />
<span>other</span>
<UserName pubkey={owner} />
</span>
)}
) : null}
<span className="opacity-50"></span>
{/* Storage status */}
{isPublished ? (
@@ -187,6 +190,7 @@ export function SpellbookDropdown() {
);
}, [localSpellbooks, networkEvents, activeAccount]);
const owner = activeSpellbook?.pubkey;
// Derived states for clearer UX
const isOwner = useMemo(() => {
if (!activeSpellbook) return false;
@@ -311,6 +315,7 @@ export function SpellbookDropdown() {
{activeSpellbook.title}
</div>
<SpellbookStatus
owner={owner}
isOwner={isOwner}
isPublished={activeSpellbook.isPublished}
isLocal={isInLibrary}
@@ -359,6 +364,7 @@ export function SpellbookDropdown() {
{activeSpellbook.title}
</div>
<SpellbookStatus
owner={activeSpellbook.pubkey}
isOwner={isOwner}
isPublished={activeSpellbook.isPublished}
isLocal={isInLibrary}
@@ -461,12 +467,12 @@ export function SpellbookDropdown() {
{sb.isPublished ? (
<Cloud
className="size-3 text-green-600 flex-shrink-0"
title="Published"
aria-label="Published"
/>
) : (
<Lock
className="size-3 text-muted-foreground flex-shrink-0"
title="Local only"
aria-label="Local only"
/>
)}
</DropdownMenuItem>

View File

@@ -35,7 +35,7 @@ import {
import type { LocalSpellbook } from "@/services/db";
import { PublishSpellbook } from "@/actions/publish-spellbook";
import { DeleteEventAction } from "@/actions/delete-event";
import { hub } from "@/services/hub";
import { hub, publishEvent } from "@/services/hub";
import { useGrimoire } from "@/core/state";
import { cn } from "@/lib/utils";
import { useReqTimeline } from "@/hooks/useReqTimeline";
@@ -44,6 +44,7 @@ import type { SpellbookEvent, ParsedSpellbook } from "@/types/spell";
import { SPELLBOOK_KIND } from "@/constants/kinds";
import { UserName } from "./nostr/UserName";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { lastValueFrom } from "rxjs";
interface SpellbookCardProps {
spellbook: LocalSpellbook;
@@ -380,16 +381,22 @@ export function SpellbooksViewer() {
const handlePublish = async (spellbook: LocalSpellbook) => {
try {
// Use hub.exec() to get the event and handle side effects after successful publish
for await (const event of hub.exec(PublishSpellbook, {
state,
title: spellbook.title,
description: spellbook.description,
workspaceIds: Object.keys(spellbook.content.workspaces),
content: spellbook.content,
})) {
const event = await lastValueFrom(
hub.exec(PublishSpellbook, {
state,
title: spellbook.title,
description: spellbook.description,
workspaceIds: Object.keys(spellbook.content.workspaces),
content: spellbook.content,
}),
);
if (event) {
await publishEvent(event);
// Only mark as published AFTER successful relay publish
await markSpellbookPublished(spellbook.id, event as SpellbookEvent);
}
toast.success("Spellbook published");
} catch (error) {
toast.error(

View File

@@ -2,7 +2,6 @@ import type { NostrEvent } from "@/types/nostr";
import { useMemo } from "react";
import { Heart, ThumbsUp, ThumbsDown, Flame, Smile } from "lucide-react";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { getContentPreview } from "./index";
import { UserName } from "../UserName";
import { RichText } from "../RichText";
@@ -83,9 +82,6 @@ export function ReactionCompactPreview({ event }: { event: NostrEvent }) {
// Fetch the reacted event
const reactedEvent = useNostrEvent(eventPointer);
// Get content preview
const preview = reactedEvent ? getContentPreview(reactedEvent, 50) : null;
// Map common reactions to icons for compact display
const getReactionDisplay = (content: string) => {
switch (content) {

View File

@@ -1,8 +1,6 @@
import type { NostrEvent } from "@/types/nostr";
import { useMemo } from "react";
import { Repeat2 } from "lucide-react";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { getContentPreview } from "./index";
import { UserName } from "../UserName";
import { RichText } from "../RichText";

View File

@@ -10,7 +10,7 @@ import {
parseHeadBranch,
getRepositoryStateHead,
} from "@/lib/nip34-helpers";
import { Label } from "@/components/ui/Label";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
/**

View File

@@ -3,7 +3,7 @@ import { Skeleton } from "./Skeleton";
export interface InlineReplySkeletonProps {
/** Icon to show on the left (Reply, MessageCircle, etc.) */
icon: React.ReactNode;
icon?: React.ReactNode;
className?: string;
}

View File

@@ -13,7 +13,7 @@ import accountManager from "./accounts";
*
* @param event - The signed Nostr event to publish
*/
async function publishEvent(event: NostrEvent): Promise<void> {
export async function publishEvent(event: NostrEvent): Promise<void> {
// Try to get author's outbox relays from EventStore (kind 10002)
let relays = await relayListCache.getOutboxRelays(event.pubkey);